предисловие
Недавно мне захотелось больше узнать об основных принципах vue.js (далее vue), и я случайно увидел туториал по изучению принципов vue.проект. После глубокого понимания я обнаружил, что он короткий и компактный, что очень полезно для постепенного понимания реализации основных принципов Vue, поэтому я официально начал его изучение.
концепция
Понятия представляют консенсус в человеческом сознании. Поэтому, чтобы добиться каких-то результатов посредством общения, очень важно достичь консенсуса по одной и той же концепции, иначе это будет болтовня о курице и утке. Какие концепции необходимо понимать в процессе понимания принципа Vue? Дальше разбираемся вместе.
DocumentFragment
Если быть точным, DocumentFragment — это веб-API. Потому что это стало почти синонимом эффективной работы большого количества узлов dom, и vue также использует его при реализации парсинга шаблонов, поэтому нам необходимо это понять.
Все следующие интерфейсы наследуются от методов и свойств Node: Document, Element, Attr, CharacterData (которые наследуют Text, Comment и CDATASection), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReference.
Поскольку наши наиболее часто используемые Element API и DocumentFragment API унаследованы от интерфейса Node, объекты DocumentFragment имеют те же методы и свойства, что и обычные объекты Element. С этой точки зрения объект DocumentFragment «такой же», как и обычный объект Element.
Но со всесторонней точки зрения это не одно и то же. По внешнему виду объект DocumentFragment отличается от объекта Element двумя моментами:
- Объект DocumentFragment не имеет
parent node
. Даже если вы добавите его к элементу в документе, этот элемент не станет егоparent node
. - В отличие от объекта Element, связанные операции DOM, выполняемые над объектом DocumentFragment, не будут немедленно отражены в интерфейсе, то есть не вызовут перекомпоновки и рендеринга (или компоновки и рисования). Давайте взглянем на простой пример кода:
const FG = document.createDocumentFragment();
const textNode = document.createTextNode('hello,documentFragment');
FG.appendChild(textNode) // 在这一步,界面并不会得到更新
document.body.appendChild(FG); // 直到把它append到真实的文档流,界面才会有反应
console.log(FG.parentNode) // 虽然FG插入到真实的文档中了,但是FG.parentNode仍然为null
Описанное выше — поверхностное явление. Какова существенная причина этого различия? Ответ: «Основная причина в том, что объект DocumentFragment не является частью реального документооборота, он находится только в памяти». поместить его (ссылаясь на объект DocumentFragment) добавить или вставить в реальный поток документов, он удаляет все, что ему принадлежит, возвращает его в реальный поток документов, а затем успешно завершает работу.
Основываясь на характеристиках этого объекта DocumentFragment, многие библиотеки классов используют его для выполнения крупномасштабных операций с узлами DOM, и vue не является исключением.
шаблон
Шаблон есть результат фиксации и стандартизации структурного закона вещи, он воплощает в себе стандартизацию структурной формы.
В библиотеке классов vue есть три типа шаблонов:
- DOM-шаблон
- шаблон строки
- Шаблон одного файла
Независимо от типа «шаблона», это, по сути, «HTML-шаблон» — набор разметки, состоящий из тегов html и специальных символов-заполнителей. «Тег html» здесь представляет твердую структуру страницы, а «специальный символ-заполнитель» состоит из набора «шаблонного синтаксиса».
Все "шаблоны" должны быть проанализированы (или скомпилированы), а проанализированные объекты - это "специальные символы-заполнители". В Vue люди Jianghu называют это «волшебным талисманом». Наконец, код, отвечающий за синтаксический анализ, мы называем «механизмом шаблонов». От усов и обработчиков в эпоху jquery до нынешних angular и vue «шаблоны» всегда были с нами. С точки зрения пользователя они не одинаковы. Но с точки зрения реализатора они все одинаковые, как "синтаксис шаблона" + "движок шаблона".
Поскольку эта библиотека классов mvvm используется для изучения принципов vue, мы должны предположить, что был разработан «синтаксис шаблона». Вопрос, над которым нам нужно подумать, звучит так: «Учитывая набор синтаксиса шаблона, как нам запрограммировать механизм шаблонов, который реализует шаблон?».
Примечание. Эта обучающая библиотека представляет собой упрощенный код, и реализация была скорректирована.
- Удалено «bind:» из имен директив привязки свойств. То есть исходный "v-bind:class" становится текущим "v-class".
- Реализовано только несколько основных директив привязки свойств ("v-text", "v-class", "v-html", "v-model") и директив привязки событий (v-on:xxx).
выражение
Выражение — это фраза в JavaScript, которую интерпретатор JavaScript оценивает для получения результата.
Книга JavaScript Rhino говорит так. Другими словами, любое утверждение, которое оценивает значение, является выражением.
В шаблоне vue независимо от того, является ли строка в двойных фигурных скобках или значение инструкции привязки свойства выражением. Основным элементом выражения является [переменная], которая соответствует атрибуту экземпляра модели представления. Если А использует В, мы говорим, что «А зависит от В», тогда мы можем преобразовать приведенное выше утверждение в следующий вывод: «В vue [шаблон] зависит от [выражения], а [выражение] зависит от [свойства экземпляра viewModel].«На самом деле, более подробное [выражение] зависит от свойств объекта данных, передаваемых при создании экземпляра объекта vue. Однако на более позднем этапе мы проксируем свойства объекта данных в свойства экземпляра viewModel.
Понимание концепции выражения и его ключевой роли между шаблонами и экземплярами модели представления имеет решающее значение. Потому что это связано с вашим пониманием семантики именования каждой роли в модели mvvm (например, «наблюдатель», «зависимость» в исходном коде и т. д.).
брокер данных
Говоря о режиме mvvm, естественно, он неотделим от агента данных. Так что же такое брокер данных?
Прокси данных на самом деле является прокси чтения и записи переменных, другими словами, [чтение и запись] исходной переменной завершается другой переменной. Например, есть объект, который находится очень глубоко:
const obj = {
a:{
b:{
c : 'xxx'
}
}
}
Каждый раз, когда мы обращаемся к атрибуту c, мы должны передатьobj.a.b.c
Если вы сделаете это слишком много раз, это будет очень хлопотно. мы можемobj.a.b.c
Делегирование чтения и записи делегируется новому свойству первого уровня объекта obj, то есть когда мы пишем такую строку кода:
obj.c = 'xxx'
Движок js поможет нам перенести операции чтения и записи вobj.a.b.c
В теле фактическое выполнение — это следующий оператор:
obj.a.b.c = 'xxx'
Простая реализация выглядит следующим образом:
Object.defineProperty(obj,"c",{
get(){
return obj.a.b.c
},
set(value){
obj.a.b.c = value;
}
})
Чтобы понять концепцию «агента данных», конкретный пример здесь должен понятьobj.a.b.c
а такжеobj.c
Отношение.
Без дальнейших церемоний, давайте подытожим отношения между ними. То есть:obj.a.b.c
Делегируйте свой собственный бизнес «чтения и записи»obj.c
готово,obj.c
даobj.a.b.c
агент.
Прокси-сервер данных до vue2.x был реализован на основе API Object.defineProperty ES5. Я считаю, что это хорошо известно, поэтому я не буду здесь его расширять (я слышал, что 3.x основан на собственном интерфейсе.Proxyреализовать). Однако я хотел бы подчеркнуть, что посредничество с данными не является необходимой функцией шаблона mvvm, это просто удобство. Конкретно почему так сказано, в процессе анализа исходного кода я объясню причины этого.
привязка данных
Мы упоминаем «привязку данных» каждый день, так что же означает «привязка данных»? Короче говоря, в контексте шаблона mvvm «привязка данных» относится к привязке свойств экземпляра viewModel к шаблону HTML, и как только значение свойства изменится, интерфейс «автоматически» обновится. Всегда обращайте внимание, «автоматический» на самом деле не автоматический, «автоматический» требует, чтобы мы реализовали его с помощью кода.
В дополнение к «привязке данных» мы часто упоминаем «одностороннюю привязку данных» и «двустороннюю привязку данных». На самом деле, в общем, «односторонняя привязка данных» относится к «привязке данных», о которой мы упоминали выше (viewModel实例属性 -》 HTML模板
), а «двусторонняя привязка данных» — добавить еще одно направление на основе «односторонней привязки данных» (HTML模板 -》 viewModel实例属性
) только привязка. В общем, «двусторонняя привязка данных» предназначена только для ввода элементов формы, выбора, текстового поля и т. д. Эта «двусторонняя привязка данных» может быть достигнута путем прослушивания входных событий этих элементов и ручного присвоения значений свойствам экземпляра viewModel в библиотеке классов.
Всегда помните, что «двусторонняя привязка данных» основана на «односторонней привязке данных». Позже, когда мы объясним реализацию кода, мы сначала поговорим о том, как реализовать «одностороннюю привязку данных», а затем о том, как реализовать «двустороннюю привязку данных».
захват данных
С причинно-следственной точки зрения «связывание данных» — это результат, а захват данных — средство для достижения этого результата. Связь между ними может быть выражена какПривязка данных достигается за счет захвата данных.
Так что же такое «захват данных»? «Похищение данных» заключается в том, чтобы лишить исходную переменную права говорить в терминах чтения и записи. После лишения я могу делать все, что захочу. В частности, «перехват данных» по-прежнему реализуется через API Object.defineProperty.
const obj = {
a:'xxx',
b:'yyy'
}
Object.defineProperty(obj,"a",{
get(){
// 劫持原属性的读的权利
// 目前我什么都不干
},
set(){
// 劫持原属性的写的权利
// 目前我什么都不干
}
})
console.log(obj.a) // undefined
obj.a = "zzz"
console.log(obj.a) // undefined
Этот захват завершен. Что это значит? Как и в приведенном выше примере, после того, как я угнал, я ничего не делаю, тогда js-движок не сделает для этого совместимый механизм понижения версии (например, как только js-движок решит, что ваш геттер возвращаетundefined
, это поможет вам вернуть исходное значение «xxx» по умолчанию). Нет, это не так. Это 100% делегировано вам. Это полностью отражает семантику слова «угон».
Возможно, вы спросите: «Проксирование данных и перехват данных реализованы через Object.defineProperty API. Принцип один и тот же. Есть ли между ними разница?»
Ответ: «Эти две концепции все еще разные. Потому что «прокси данных» должен генерировать новый атрибут объекта, а «перехват данных» — переопределять существующие атрибуты объекта». исходный код, у меня также была похожая путаница.Я неоднократно проверял реализацию кода двух и нашел разницу между ними. Если у вас есть тот же вопрос, не вините себя, просто прочитайте исходный код несколько раз.
На этом этапе мы разобрались с задействованными концепциями, а затем представим процесс реализации каждого функционального модуля.
Общий процесс
Подробную блок-схему я нарисовал:
Здесь я разделяю жизненный цикл кода библиотеки классов vue на две фазы: фазу инициализации и фазу выполнения.
Фаза инициализации подразделяется на три подзаготовления:
- Стадия брокера данных
- этап привязки данных
- Фаза разбора шаблона.
Итак, как показано на рисунке выше, всего имеется четыре этапа. Далее давайте обсудим процесс реализации и принципы каждого этапа.
1. Этап прокси данных
На данном этапе говорить, собственно, не о чем, основной принцип — API Object.defineProperty — то есть через этот API права чтения и записи vm._data делегируются свойствам экземпляра vm. Здесь стоит отметить два момента:
- Прокси данные не являются необходимой особенностью модели MVVM, это просто удобство.
- Посредничество с данными отличается от кражи данных. Разница заключается в следующем:
- В отличие от перехвата данных, который требует рекурсивного обхода всех уровней объекта данных, проксирование данных — это всего лишь прокси атрибута первого уровня для vm._data.
- В отличие от перехвата данных, который переопределяет исходные атрибуты, проксирование данных заключается в создании новых атрибутов экземпляра виртуальной машины.
Теперь давайте подробно обсудим два вышеуказанных момента.
Во-первых, давайте попробуем, чтобы увидеть, может ли программа работать нормально, если прокси-сервер данных отключен. Как это сделать?
Шаг 1: Закомментируйте код, связанный с реализацией прокси данных в конструкторе mvvm.js MVVM.
Шаг 2: Перейдите в метод получения, какой класс Watcher, второй аргумент при вызове параметров, передаваемых из альтернативного метода этого. GTETTER "this.vm" на "this.vm._data".
Шаг 3: Перейдите к методу привязки класса Compile и замените первый параметр вызова метода this._getVMVal с «vm» на «vm._data».
Наконец, сохраните обновление и обновите страницу. Вы обнаружите, что функция привязки данных не затрагивается, и программа работает нормально. Это также подтверждает мой первый тезис.
Однако теперь, если вы хотите изменить значение данных, вы не можете напрямую работать с экземпляром vm. То есть уже нельзя писать:this.xxx = 'yyy'
илиvm.xxx = 'yyy'
. Вместо этого напишите это:this._data.xxx = 'yyy'
илиvm._data.xxx = 'yyy'
. При этом мы сталкиваемся с проблемой. Что, если свойство, к которому мы хотим получить доступ, находится на глубоком уровне? Например:a.b.c
, то надо написатьthis._data.a.b.c = 'yyy'
илиvm._data.a.b.c = 'yyy'
. Можно сделать это один раз, но часто этого недостаточно, а простой прокси-сервер данных может помочь нам сократить иерархию атрибутов, сделав доступ к данным более интуитивным. Думаю, в этом смысл существования брокеров данных.
Что касается второго пункта, мы можем это выяснить, внимательно изучив коды реализации [Data Proxy] и [Data Hijacking].
// 数据代理实现代码
// 这个data就是我们传递到Vue构造函数的的option对象的data字段
Object.keys(data).forEach(function(key) {
me._proxyData(key);
});
_proxyData: function(key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
// 数据劫持实现代码
Observer.prototype = {
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
};
Хотя и проксирование данных, и перехват данных реализованы через Object.defineProperty API, [объекты], на которые они нацелены (то есть первый параметр, передаваемый при вызове), очевидно, отличаются.
Для агента данных наш [объект] — это экземпляр виртуальной машины, а определенное [свойство] — это свойство объекта данных. С точки зрения конструктора MVVM экземпляр vm не определяет эти свойства раньше, и эти свойства не существуют при вызове метода Object.defineProperty(). Следовательно, это совершенно новые свойства экземпляра vm; и для захвата данных наш [объект] является объектом данных, а определенное [свойство] по-прежнему является свойством объекта данных, так что это переопределение. Именно это новое определение так хорошо перекликается с семантикой слова «угон», не так ли?
Что касается разницы в количестве уровней атрибутов объекта, управляемых прокси-сервером данных и перехватом данных, то это в основном отражается в том факте, что прокси-сервер данных выполнялся только один раз.Object.keys().forEach()
Вызывается для перебора свойств первого уровня объекта данных. А захват данных осуществляется вызовом метода defineReactive.var childObj = observe(val);
вызов, косвенный рекурсивный вызов несколько разObject.keys().forEach()
Добиться захвата всех иерархических свойств объекта данных.
До сих пор мы разбирали детали реализации прокси данных, сравнивая реализацию функции перехвата данных. Можно сказать, что прокси данных — это лишь закуска к модели mvvm, а привязка данных — ее основная часть, давайте посмотрим на процесс ее реализации.
2. Этап привязки данных
Как упоминалось в разделе, посвященном концепции, «связывание данных» — это результат, которого мы хотим достичь, а не суть. Дело в том, что средство для достижения этого результата — захват данных. Таким образом, в этом разделе речь идет не о «привязке данных», а о «перехвате данных».
Код реализации «перехвата данных» размещен в файлеObserver.js. Во всем файле не так много строк кода, но поскольку автор этой библиотеки классов только что выдернул из исходного кода vue, он короткий и лаконичный, что очень удобно для чтения.
Будь то в разделе введения концепции или на предыдущем этапе, мы кратко представили процесс захвата данных. Если описать этот процесс одним предложением, то это:Пройдите каждый уровень свойств, которые мы передаем в объекте данных, и установите соответствующие методы доступа (геттеры и сеттеры) для каждого свойства.. Хотя он предполагает несколько более сложный [косвенный рекурсивный вызов], этот процесс все равно прост и интуитивно понятен, и тут не о чем говорить. Мы хотим поговорить о наборе аксессоров в процессе захвата данных, потому что в нем скрыта очень важная цепочка отношений:
экземпляр dep -> свойства -> выражение -> экземпляр наблюдателя
На этапе инициализации создание этой цепочки отношений делится на четыре этапа:
- Учреждать
属性 《-》 表达式
Многозначные отношения (фаза написания шаблонов) - Учреждать
属性 《-》 dep实例
отношение один к одному (фаза привязки данных) - Учреждать
表达式 《-》 watcher实例
Отношение один к одному (фаза разбора шаблона) - Учреждать
dep实例 《-》 watcher实例
Связь «многие ко многим» (фаза анализа шаблона)
Связь «многие ко многим» между свойствами и выражениями устанавливается на этапе написания нашего шаблона. Связь «многие ко многим», что это значит? Это означает, что выражение может «зависеть» или «использовать» несколько свойств, и одно и то же свойство может использоваться несколькими выражениями. См. конкретные примеры:
<div>
<div>第一个表达式:{{a.b.c}}</div>
<div>第二个表达式:{{a.b.c}}</div>
<div>第三个表达式:{{a.b.c}}</div>
</div>
Давайте пока посмотрим на первое выражение. Поскольку это выражение использует три свойства: «a», «ab» и «a.b.c», мы говоримВыражение может соответствовать нескольким свойствам. Тогда давайте посмотрим на шаблон целиком. Одно и то же свойство «a.b.c» используется тремя выражениями, поэтому мы говоримАтрибут может соответствовать нескольким выражениям.
Подводя итог, можно сказать, что атрибуты и выражения имеют отношения «многие ко многим».
На этапе привязки данных причина анализа отношений между атрибутами и выражениями заключается в том, что эти отношения являются корнем всей цепочки отношений и предзнаменованием [отношений между атрибутами и экземплярами dep]. Хорошо, теперь мы закончили. Таким образом, мы можем сосредоточиться на том, что происходит на этом этапе.Установлена связь между атрибутом и экземпляром DEP.. Установление этой связи происходит при перехвате метода доступа к определению свойства. Итак, давайте посмотрим на соответствующий код:
Observer.prototype = {
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
// 记住,this.data只是vm._data的一个引用
// 引用链是这样的: this.data -> vm.$option.data -> vm._data
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
// 这里通过闭包,将key所对应的dep实例以及对之间的对应关系保存在内存当中了
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知所有订阅者。这里的订阅者就是watcher实例
dep.notify();
}
});
}
}
Мы всегда должны помнить, что объект данных, передаваемый при создании экземпляра vue, передается по ссылке, а объект данных в формальном параметре в Object.defineProperty — это объект данных, который мы передали. существуетdefineReactive方法
в, черезvar childObj = observe(val);
Это предложениеdefineReactive方法
Выполняются косвенные рекурсивные вызовы, тем самым проходя все уровни объекта данных. В процессе обхода первое, что он делает, это создает новыйDep实例
. Но здесь свойства иDep实例
Связь один-к-одному пока установить нельзя, потому что, как видите, новаяDep实例
, мы не передавали никаких данных, связанных с этим свойством, вDep实例
. Как устанавливаются отношения между ними? Ответ: "Закрыта".
существуетdefineReactive方法
Существует два уровня лексического объема. Первый слойdefineReactive方法
Сам по себе второй уровень — это функции получения и установки свойства. Поскольку оба уровня доступа к вложенным областямdep
Таким образом, эта переменная в нашем коде образует видимое замыкание. когдаdefineReactive方法
Когда среда выполнения фактически вызывается, наш код создает замыкание. Именно это замыкание хранит однозначное соответствие между текущим атрибутом и текущим экземпляром dep в памяти, ожидая нашего дальнейшего использования.
Можно сказать, что этап захвата данных завершает установление однозначного соответствия между атрибутами и экземплярами dep. Не только это, но и для спиныУстановлена связь между экземпляром наблюдателя и экземпляром dep.Было заложено предзнаменование. Где это предчувствие? Да, в геттере:
// ....此前省略了很多代码
get: function() {
// 对的,就是这三行简简单单的代码
if (Dep.target) {
dep.depend();
}
return val;
},
// ....此后省略了很多代码
Когда программа входит в замыкание, которое мы только что упомянули на следующем этапе (этап разбора шаблона), выполнениеdep.depend()
Когда делается это утверждение, официально начинается установление связи между экземпляром наблюдателя и экземпляром dep. Что ж, переходим к следующему этапу анализа процесса.
3. Фаза разбора шаблона
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
Из кода реализации обучающей библиотеки синтаксический анализ шаблона можно разделить на три этапа:
- Из реального узла контейнера переместите все узлы в контейнер DocumentFragment.
- В контейнере DocumentFragment выполните весь анализ шаблонов и манипуляции с DOM.
- Переместите узел в контейнере DocumentFragment обратно в реальный узел контейнера.
Чтобы понять код реализации шага 1, в основном необходимо понять объект DocumentFragment и API appendChild(). Для понимания объекта DocumentFragment мы уже объяснили его в разделе «Сортировка понятий», поэтому я не буду повторяться здесь. Для API appendChild() самым важным в первую очередь является понимание предложения «каждый узел элемента может иметь только один родительский узел». Если вы не понимаете это предложение, значит, вы запутались в основном коде реализации шага 1:
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
когда ты видишьfragment.appendChild(child)
Во время этого утверждения вы могли подумать, добавить существующий узел кfragment
После объекта не удалять? Ответ: «Нет». Именно за это отвечает API appendChild(). Этот API по-прежнему следует принципу «каждый узел элемента может иметь только один родительский узел». Следовательно, после цикла все узлы в контейнере реальных узлов переносятся вfragment
объект внутри.
После того, как шаг 1 завершен, шаг 2 является наиболее важным. Теперь давайте посмотрим на шаг 2.
На этом этапе мы в основном следуем двум отношениям, которые не были упомянуты на предыдущем этапе, чтобы объяснить:
- Учреждать
表达式 《-》 watcher实例
Отношение один к одному (фаза разбора шаблона) - Учреждать
dep实例 《-》 watcher实例
Связь «многие ко многим» (фаза анализа шаблона)
Как описано в подробной блок-схеме, которую я привел выше, в нижней части всего процесса разбора шаблона выполняются две вещи:
- Завершено начальное отображение интерфейса
- Начните работать над созданием экземпляра наблюдателя
Говоря профессиональным языком, самая верхняя функция bind() в стеке вызовов функции compileElement() делает две вещи:
......
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 1. 完成了界面的初始化显示
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 2. 开始着手实例化watcher
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
......
Из приведенного выше кода реализации мы можем интуитивно увидеть код, соответствующий этим двум вещам:
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); });
О коде реализации первого и говорить нечего, т.к. здесь выполняется код, определяются три элемента работы DOM: узел (node), операция (updaterFn) и значение (this._getVMVal(vm, exp)) , Так что дальшеПрисвоить значение атрибуту на конкретном узле.
Код реализации второй вещи — суть этапа разбора шаблона.
// 实例化Watcher类
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
// Watcher类的构造函数
function Watcher(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get();
}
Либо формальный параметр exp, либо expOrFn ссылаются на выражение в шаблоне. отnew Watcher(vm, exp,...)
прибытьthis.expOrFn = expOrFn;
, мы можем видеть интуитивноУстановлено однозначное соответствие между экземплярами наблюдателя и выражениями..
Ну, пока у нас осталосьСвязь между экземпляром dep и экземпляром watcherНе проанализировано. Чтобы понять, как устанавливается взаимосвязь между ними, мы должны продолжить отслеживать стек вызовов функций, участвующих в создании экземпляра наблюдателя.
Перед трассировкой мы должны иметь в уме концепцию, то есть экземпляр dep уже существует в памяти, и он ждет, чтобы зажечь предохранитель, устанавливающий отношения между ними во время процесса инстанцирования наблюдателя.
Помните то замыкание, о котором мы говорили на предыдущем этапе?
когда
defineReactive方法
Когда среда выполнения фактически вызывается, наш код создает замыкание. Именно это замыкание хранит однозначное соответствие между текущим атрибутом и текущим экземпляром dep в памяти, ожидая нашего дальнейшего использования.
Очевидно, что внутренней лексической областью видимости этого замыкания является геттер-функция. И предохранитель, о котором мы говорим, находится в функции геттера.dep.depend();
утверждение. Поскольку предохранитель находится в функции получения свойства (также называемой средством доступа к свойству), как следует из названия, как только мы прочитаем значение свойства, мы «зажжем» корневой предохранитель. В процессе создания экземпляра наблюдателя, где вам нужно прочитать значение свойства?
Мы следовали коду, связанному с созданием экземпляра наблюдателя, и нашли следующее утверждение:
this.value = this.get();
Правильно, здесь именно этот оператор (цель — вычислить значение выражения, соответствующего текущему экземпляру наблюдателя) поджигает предохранитель, устанавливающий отношения между экземпляром наблюдателя и экземпляром dep. Строго говоря, перед тем, как экземпляр dep фактически установит отношения с экземпляром наблюдателя, он фактически должен «постучаться в две двери». Какие две двери?
Первая дверьDep.target
, это внутри метода доступа геттера свойства:
if (Dep.target) {
dep.depend();
}
Вторая дверьthis.depIds.hasOwnProperty(dep.id)
, который находится в методе addDep экземпляра наблюдателя:
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
Видно, что первая дверь будет открыта только тогда, когда значение статического атрибута target класса Dep не равно ложному значению; вторая дверь будет открыта только в том случае, если текущий экземпляр dep не установил отношения с текущим экземпляр наблюдателя.
Что ж, сначала давайте посмотрим на состояние первого дверного выключателя. Первая дверь сначала закрыта. Соответствующий код — это последняя строка кода в файлеObserver.js:
Dep.target = null;
Когда он открылся? мы могли бы также вернуться кthis.value = this.get();
В этой строке кода мы продолжаем возвращаться к вершине стека вызовов функции this.get(). Конечно же, в коде реализации метода get() экземпляра наблюдателя мы видим такой оператор:
Dep.target = this;
Вы правильно прочитали, первая дверь была открыта. Следующее утверждениеthis.getter.call(this.vm, this.vm);
, для его выполнения программа войдет в метод доступа геттера свойства и начнет путь установления отношений.
Мы можем думать о том, что следует, как о видеоклипе. Первый кадр в этом видеоклипе — человек, стоящий перед дверью. Этот человек называется "экземпляром (в памяти) dep". Я видел, как подчиненный экземпляр мягко постучал в "будуарную дверь" экземпляра наблюдателя и сказал: "Дорогой экземпляр наблюдателя, наконец-то вы открыли дверь, тогда давайте установим отношения". Например, наблюдатель все же обнял пипу и сказал: «Не волнуйся, гость, у тебя еще есть вторая дверь, которую нужно открыть?».
Итак, деп экземпляр подошел к двери второй двери. Когда он это увидел, оказалось, что дверь открыта (до установления связи свойство depIds экземпляра наблюдателя точно не имело ссылки на текущий экземпляр dep). Я подумал про себя: «Эта сука очень хорошо умеет притворяться, что лжет мне. Дверь вовсе не закрыта». Таким образом, экземпляр dep входит прямо в систему. Объектив здесь....
Что ж, теперь экземпляр депа прошел две двери и благополучно вошел в будуар экземпляра наблюдателя. Оба готовы к отношениям. Основное заявление, отвечающее за установление двусторонних отношений, состоит всего из двух строк кода, а именно:
dep.addSub(this);
this.depIds[dep.id] = dep;
Наконец, давайте закончим анализ на этом этапе, ответив на вопрос «Что значит установить связь между экземпляром dep и экземпляром watcher?»
Функция первой строки кода состоит в том, чтобы понять, что экземпляр dep активно устанавливает связь с экземпляром наблюдателя. На языке кода это означает сохранить текущий экземпляр наблюдателя в массиве subs (подписчик) атрибута экземпляра dep и дождаться уведомления (вызвать метод update() экземпляра наблюдателя);
Роль второй строки кода состоит в том, чтобы понять, что экземпляр наблюдателя активно устанавливает связь с экземпляром dep. На языке кода соответствующий ключ-значение устанавливается в объекте depIds атрибута экземпляра наблюдателя, чтобы сохранить ссылку на текущий экземпляр dep. Это действует как заглушка доступа для экземпляра dep. В следующий раз, когда вы установите отношение и обнаружите, что у этого экземпляра отложения уже есть заглушка, вы можете отклонить его.
Выше мы разобрали весь процесс установления связи «многие ко многим» между экземпляром dep и экземпляром watcher. А почему многие ко многим? Мы уже говорили об этом на этапе сортировки понятий выше, а теперь повторим еще раз. Мы также должны помнить, что выражения могут состоять из n атрибутов. Следовательно, чтение значения выражения, скорее всего, приведет к n-кратному чтению значения атрибута. . n атрибутов соответствуют n экземплярам dep, а чтение n значений атрибутов означает, что n раз установление отношений происходит в течение одного процесса инстанцирования наблюдателя. С другой точки зрения, шаблон может иметь m выражений, а m выражений означают m экземпляров наблюдателя. m *n, в конце концов, экземпляр dep формирует отношения «многие ко многим» с экземпляром наблюдателя.
Ну вот, анализ процесса на данном этапе завершен. Если подытожить этот этап с точки зрения того, сделали ли вы что-то практическое, то на этом этапе было сделано только одно практическое дело. То есть завершить начальное отображение интерфейса. Остальное делается на этапе выполненияГотов к работе. Хорошо, давайте посмотрим, как препараты, сделанные на этом этапе, получаются на следующем этапе.
4. Фаза выполнения
Так называемая среда выполнения Очевидно, такой этап заключается в назначении атрибутов для запуска соответствующего выполнения кода. Во vue внутри, либо в методе, либо в нашем кастоме, для vue его основные операции по-прежнему являются «назначением атрибутов» в обработчике событий. Это настолько простое задание, что давайте всю подготовительную работу проделаем на сцене.
На самом деле, прежде чем войти в установщик свойств перехвата данных, необходимо пройти через зарегистрированный получатель данных прокси, а затем после получателя свойства перехвата данных и, наконец, в установщик свойств перехвата данных. Однако, поскольку в это время Dep.target имеет значение null, значение атрибута не может быть прочитано первой дверью. Таким образом, такие потрясения, чтобы резко закончиться здесь. Нам просто нужно сосредоточиться на путешествии (назначении свойства) терминала, например, то есть на средстве доступа к установке свойств для захвата данных. Давайте посмотрим на конкретный код:
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
На самом деле сеттер делает три вещи.
- Если новое значение равно старому, ничего не делайте.
- В противном случае захватите свойство, содержащееся в новом значении.
- Наконец, все экземпляры наблюдателя, подписанные на этот экземпляр dep, получают уведомление. Средство обновления, ответственное за обновление интерфейса, было связано с каждым экземпляром наблюдателя посредством обратного вызова на предыдущем этапе, поэтому здесь экземпляр наблюдателя может легко вызвать средство обновления для достижения общего обновления интерфейса.
Причина, по которой назначение экземпляра vm может войти в установщик свойства перехвата данных, причина, по которой экземпляр dep может уведомить экземпляр наблюдателя, причина, по которой экземпляр наблюдателя может вызвать средство обновления, и все потому, что мы сделали достаточно на предыдущих трех этапах. Будьте готовы к тому, чтобы это произошло.
На данный момент проанализированы все четыре этапа. Основное внимание уделяется установлению связи «многие ко многим» между экземплярами dep и экземплярами Watcher. фактически"Установление отношения «многие ко многим» между экземплярами dep и экземплярами watcher.«Есть еще один вариант, который называется «коллекция зависимостей». Оглядываясь назад, мы можем понять концепцию «коллекции зависимостей» следующим образом: если мы скажем: «Кто бы ни использовал того, кто зависит от того, кто есть кто». Тогда выражение использует Если мы знаем свойство, мы говорим: «Выражение зависит от свойства». Далее мы можем думать об экземпляре dep как о брокере свойства, а о наблюдателе — как о стюарде выражения. Стюард отвечает за сбор всех зависимостей Атрибуты, когда он находит соответствующие атрибуты один за другим, эти атрибуты говорят стюарду-наблюдателю: «В чем дело, расскажи мне о экземпляре dep моего брокера». В конце концов, «коллекция зависимостей» становится экземпляр dep и наблюдатель Это вопрос между экземплярами.Короче говоря, «сбор зависимостей» можно понимать как «экземпляр наблюдателя заменяет выражение для сбора экземпляра dep зависимых свойств последнего».
резюме
С точки зрения атрибутов объекта данных мы можем увидеть взаимосвязь между атрибутами, выражениями, экземплярами dep и экземплярами наблюдателя, чтобы установить диаграмму плавательных дорожек следующим образом:
технический пункт
Эта обучающая библиотека предполагает применение множества технических моментов, ниже сделаем простую запись.
Закрытие
Наиболее очевидными и важными замыканиями являются следующие три:
1. Доступ к переменной dep вложенной лексической области defineReactive осуществляется в геттере или сеттере вложенной лексической области видимости:
//在observer.js文件里面
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
- Доступ к переменным updaterFn и node вложенной функции связывания с лексической областью видимости осуществляется во вложенной функции обратного вызова с лексической областью видимости:
//在compile.js文件里面
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
- Доступ к переменной exps вложенной функции parseGetter с лексической областью видимости осуществляется внутри вложенной функции возврата с лексической областью видимости:
//在watcher.js文件里面
parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
рекурсия
Существует два типа прямой рекурсии или косвенной рекурсии:
- Косвенный рекурсивный вызов defineReactive() через наблюдение(val)
//在observer.js文件里面
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
- В процессе парсинга шаблона, в процессе обхода дочерних узлов напрямую рекурсивно вызывать compileElement()
//在compile.js文件里面
compileElement: function(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
пройти по ссылке
Для поля данных объекта option, который мы передали, существует длинная цепочка передачи ссылок:
我们实例化传入的option对象 => mvvm实例的this.$options => mvvm实例的this.$options.data => mvvm实例的this._data => observe(data, this) => observer实例的this.data
Итак, в конце концов, объект захвата данных — это объект данных, передаваемый при создании экземпляра mvvm.
У узла элемента может быть только один родительский узел.
API appendChild() используется для «копирования» всех дочерних узлов реального узла контейнера в объект DocumentFragment в файле compile.js. Это подтверждает это правило в мире DOM.
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
Массивные вызовы методов массива
При выполнении манипуляций с DOM мы не можем избежать работы с массивами (некоторые люди называют это псевдомассивами).
compileElement: function(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
// ......
});
},
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// ......
});
},
Будь то [].slice.call() или Array.prototype.slice.call(), цель заимствования истинного метода массива достигнута. Однако лично я считаю, что в теории последнее было бы лучше. Поскольку последний сохраняет ненужные поиски атрибутов, производительность будет выше.
Суммировать
Если это действительно так, как сказал автор этой обучающей библиотеки (большая часть кода взята из исходного кода vue), то я считаю, что понял основные принципы vue. Что касается наложенных, менее основных функций, таких как: виртуальный DOM, механизм компонентов, различные механизмы расширения и т. д., мне нужно углубиться в реальный исходный код Vue для изучения.
Вся статья состоит почти из 10 000 слов, которые используются для поэтапного изложения изучения принципов vue для себя. Если есть какие-либо ошибки, пожалуйста, не стесняйтесь указывать на них, большое спасибо.
Наконец, спасибо за чтение.