Vue, несомненно, очень хорошая front-end MVVM-библиотека, я с любопытством начал рассматривать исходный код Vue, конечно же, у меня возникло много вопросов, а также я проверил много информации и прочитал несколько статей. Но многие из этих материалов игнорируют очень важные части или некоторые важные детали, или некоторые очень важные части не указываются, особенно при выполнении вычислений. Поэтому я планирую написать эту статью, чтобы записать свой процесс обучения.Конечно, я также надеюсь дать ссылку на другую детскую обувь, которая хочет знать исходный код VUE. Если автор где-то не так понял, я также приветствую критику и исправления, и давайте учиться вместе.
Чтобы углубить понимание, я построил простое колесо в соответствии с идеей исходного кода Реализация базового ядра согласуется с исходным кодом VUE.тестовая демонстрация. Адрес склада:eltonchan/rollup-ts
Исходный код VUE принимаетrollupа также flowЧто касается того, почему бы не использовать машинописный текст, основное внимание уделяется стоимости и выгоде разработки, что особенно упоминается в Zhihu. (Версия 3.0+ вместо этого использует typeript)
Почему Vue 2.0 выбирает Flow для статической проверки кода вместо прямого использования TypeScript
Неважно, если вы не разбираетесь в роллапе и машинописном тексте, проект уже настроен, вам нужно только выполнить npm i (или cnpm i) для установки соответствующих зависимостей, а затем запустить npm start. npm run build builds, по умолчанию выводится формат umd, если вам нужен cmd или amd, вы можете изменить его в файле конфигурации rollup.config.js.
output: {
file: 'dist/bundle.js',
format: 'umd',
name: 'myBundle',
sourcemap: true
}
вопросы ? Понимание вещей с помощью вопросов часто приводит к большей пользе, поэтому давайте начнем со следующих вопросов.
- Как проксировать доступ с http://this.xxx на http://this._data.xxx?
- Как реализовать перехват данных и отслеживать операции чтения и записи данных?
- Как реализовать зависимый кеш?
- Как очистить набор зависимостей при изменении шаблона, например: v-if , уничтожение компонента
- Как реализовать обновление dom модификации данных?
Vue реализует принцип двусторонней привязки, в основном используя геттер/сеттер Object.defineProperty (фактически, большинство адаптивных библиотек программирования используют эту реализацию, например отличный mobx.js) и режим публикации-подписки (определяет объект). зависимость между двумя, когда состояние объекта изменяется, все объекты, которые зависят от него, будут уведомлены), а в vue наблюдатель является подписчиком, а зависимость «один ко многим» относится к данным. атрибуты наблюдателя и наблюдателя, а также то, как атрибуты данных связаны с наблюдателем, dep — это мост, поэтому, если вы понимаете отношения между dep, наблюдателем и наблюдением, вы, естественно, поймете принцип двух- способ привязки.
1. Прокси Возвращаясь к первому вопросу, ответ собственно такой: для каждого ключа на данных сделать прокси на вм, фактическая операция это ._data, код реализации такой:
export function proxy (target: IVue, sourceKey: string, key: string) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return this[sourceKey][key];
},
set(val: any) {
this[sourceKey][key] = val;
}
});
}
Можно видеть, что и получение, и изменение this.xx являются получением или изменением this.data.xx;
Два, наблюдатель для данных атрибута о свойствах пакета для просмотра Object.DefineProperty Object для перехвата с помощью Reader GetTet / Setter, собранная во время приобретения зависимости, зависимости от уведомлений, связанных с модификацией во времени.
walk(data): void {
if (!data || typeof data !== 'object') return;
Object.keys(data).forEach(key => {
this.defineReactive({
data,
key,
value: data[key]
});
});
}
defineReactive({ data, key, value }: IReactive): void {
const dep = new Dep();
this.walk(value);
const self = this;
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
Dep.target.addDep(dep);
}
return value;
},
set(newVal: any): void {
if (value === newVal) return;
self.walk(value);
value = newVal;
dep.notify();
}
});
}
Видно, что зависимости собираются во время get, а Dep.target на самом деле является наблюдателем. Мы вернемся позже, когда будем говорить о наблюдателе. Здесь следует обратить внимание на dep. На самом деле dep здесь является замыкающей средой , и при выполнении get или set Вы также можете получить доступ к созданному отл. Например, this.name создаст экземпляр Dep при получении значения this.name и добавит наблюдателя к этому dep.
Почему нельзя отслеживать недавно добавленные свойства объекта и почему можно изменить весь объект, чтобы обнаружить изменения в подсвойствах?
Из-за ограничений JavaScript Vue не может обнаружить добавление или удаление свойств объекта (конечно, mobx не является исключением). Таким образом, добавление или удаление наблюдаемых свойств объекта не может инициировать метод set, но непосредственное изменение объекта может.В методе set он определяет, является ли новое значение типом массива объектов.Если это так, подсвойства инкапсулируются в наблюдаемые свойства. Это также роль self.walk(value); в наборе.
3. Наблюдатель Наблюдатель был упомянут только что, и функцию наблюдателя можно увидеть на рисунке выше.На самом деле, каждое вычисляемое свойство и наблюдатель создают новый наблюдатель. Об этом поговорим дальше. Давайте сначала посмотрим на реализацию наблюдателя.
constructor(
vm: IVue,
expression: Function | string,
cb: Function,
) {
this.vm = vm;
vm._watchers.push(this);
this.cb = cb || noop;
this.id = ++uid;
// 处理watch 的情况
if (typeof expression === 'function') {
this.getter = expression;
} else {
this.getter = () => vm[expression];
}
this.expression = expression.toString();
this.depIds = new Set();
this.newDepIds = new Set();
this.deps = [];
this.newDeps = [];
this.value = this.get();
}
Выражение здесь — это метод рендеринга для инициализации наблюдателя, используемого для рендеринга представления, выражение для вычисляемого и ключ для наблюдателя, поэтому здесь нам нужно решить, является ли это строкой или функцией, а метод получения — используется для получения значения. Здесь есть depId и deps, но есть также newDepId и newDeps. Почему они устроены именно так? Давайте поговорим об этом далее. Давайте сначала посмотрим на this.value = this.get(); вы можете видеть, что значение здесь назначается наблюдатель, а затем посмотрите на метод get.
get() :void {
Dep.target = this;
const value = this.getter.call(this.vm); // 执行一次get 收集依赖
Dep.target = null;
this.cleanupDeps(); // 清除依赖
return value;
}
Видно, что геттер используется для получения значения.При выполнении этой строки кода на примере наблюдателя за рендерингом будет выполнен рендеринг VNode.Когда встречается выражение {{ msg }}, значение будет получено.В это время будет запущен метод get msg, а Dep.target в это время является наблюдателем, поэтому мы свяжем наблюдателя рендера с атрибутом msg, то есть dep msg уже имеет наблюдателя рендера. Это отношения между Watcher, Dep и Observer. Давайте снова посмотрим на Депа:
export default class Dep implements IDep {
static target: any = null;
subs:any = [];
id;
constructor () {
this.id = uid++;
this.subs = [];
}
addSub(sub: IWatcher): void {
if (this.subs.find(o => o.id === sub.id)) return;
this.subs.push(sub);
}
removeSub (sub: IWatcher) {
const idx = this.subs.findIndex(o => o.id === sub.id);
if (idx >= 0) this.subs.splice(idx, 1);
}
notify():void {
this.subs.forEach((sub: Isub) => {
sub.update();
})
}
depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
}
}
Реализация Dep очень проста.Глядя на метод уведомления здесь, мы знаем, что при изменении атрибута в данных запускается набор, а затем запускается метод уведомления.Тогда мы знаем, что подпрограмма является наблюдателем, поэтому метод watcher.update заключается в изменении метода attribute., вернитесь к наблюдателю, чтобы увидеть реализацию этого обновления.
update() {
// 推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this);
}
run(cb) {
const value = this.get();
if (value !== this.value) {
const oldValue = this.value;
this.value = value;
cb.call(this.vm, value, oldValue);
}
}
Метод обновления не визуализирует vNode напрямую. Вместо этого наблюдатель помещается в очередь.На самом деле vue обновляет DOM асинхронно.Почему очередь должна обновляться асинхронно?Вот выдержка из описания на официальном сайте: Vue выполняет обновления DOM асинхронно. Как только наблюдается изменение данных, Vue открывает очередь и буферизует все изменения данных, которые происходят в том же цикле событий. Если один и тот же наблюдатель запускается несколько раз, он будет помещен в очередь только один раз. Эта дедупликация во время буферизации важна, чтобы избежать ненужных вычислений и манипуляций с DOM. Затем, в следующем цикле событий «тик», Vue очищает очередь и выполняет фактическую (дедублированную) работу. Vue внутренне пытается использовать собственные Promise.then и MessageChannel для асинхронных очередей.Если среда выполнения не поддерживает это, вместо этого будет использоваться setTimeout(fn, 0). На самом деле, это очень хорошее решение для оптимизации производительности.Представим, что если назначение циклически назначается в монтируемом, если стратегия асинхронного обновления не принята, то каждое назначение обновляется, что совершенно бесполезно.
В-четвертых, nextTick На самом деле, многие статьи о nextTick написаны хорошо, поэтому я не буду подробно описывать их здесь. С задействованными концепциями можно ознакомиться, нажав на ссылку ниже:
Подробное объяснение механизма работы JavaScript: снова поговорим о цикле событий
5. Вычисляемые свойства Вычисляемые свойства кэшируются на основе их зависимостей. Они переоцениваются только при изменении соответствующих зависимостей. Вернемся к вопросу в начале, как реализовать кэширование зависимостей? Как обновление имени обновляет информацию, и если имя не меняется, как информация получает значение?
Только что говоря о наблюдателе, я упомянул, что каждое вычисляемое свойство создает экземпляр наблюдателя.Из следующего кода также видно, что у каждого вычисляемого свойства есть наблюдатель-подписчик.
initComputed(computed) {
if (!computed || typeof computed !== 'object') return;
const keys = Object.keys(computed);
const watchers = this._computedWatchers;
let i = keys.length;
while(i--) {
const key = keys[i];
const func = computed[key];
watchers[key] = new Watcher(
this,
func || noop,
noop,
);
defineComputed(this, key);
}
}
См. этот пример:
computed: {
info() {
console.info('computed update');
return this.name + 'hello';
}
},
Геттер-метод наблюдателя является выражением вычисляемого свойства, и когда this.value = this.get(), это значение будет результатом выражения, поэтому фактически Vue сохраняет значение info в значении своего свойства. watcher Inside, а затем знайте, что когда значение имени будет взято, будет запущен метод get имени.В это время Dep.target является наблюдателем этой информации, а dep — замыканием, или dep, который собрал имя раньше, так что имя Деп будет иметь двух наблюдателей, [renderWatcher, ComputedWatcher].Когда имя будет обновлено, два наблюдателя-подписчика получат уведомление, что означает, что обновление имени также обновит информацию.
Значение info — это значение наблюдателя, поэтому нам нужно сделать здесь прокси, чтобы проксировать значение вычисляемого атрибута на значение соответствующего наблюдателя, что очень просто реализовать.
export default function defineComputed(vm: IVue, key: string) {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
const watcher = vm._computedWatchers && vm._computedWatchers[key];
return watcher.value;
},
});
}
6. Обновление зависимостей
<p v-if="switch">{{ name }}</p>
Предполагая, что переключатель переключен с true на false, нужно удалить renderWatcher над именем, поэтому для записи dep необходимо использовать атрибуты depIds и deps.
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.depIds.has(dep.id)) {
dep.removeSub(this);
}
}
const tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
const deps = this.deps;
this.deps = this.newDeps;
this.newDeps = deps;
this.newDeps.length = 0;
}
Здесь newDepIds присваивается depIds, а затем newDepIds очищается.Deps также является той же операцией, которая является очень эффективной операцией и позволяет избежать использования глубокого копирования. При добавлении зависимостей для записи используются newDepIds и newDeps. При удалении он будет проходить и искать в deps. После удаления присваивать newDepIds depIds. Это гарантирует, что при обновлении зависимостей неиспользуемые зависимости будут удалены из этого наблюдателя. .
Семь, смотри Зачем смотреть объект, когда oldValue == value ?
Свойство watch также является созданным экземпляром Watcher, но выражение в этот момент — это key, значение — vm[key], а cb — функция обратного вызова, поэтому этот Watcher, естественно, в это время находится в dep соответствующего свойства.
initWatch(watch) {
if (!watch || typeof watch !== 'object') return;
const keys = Object.keys(watch);
let i = keys.length;
while(i--) {
const key = keys[i];
const cb = watch[key];
new Watcher(this, key, cb);
}
}
Когда свойство будет обновлено, будет выполнен метод запуска.Когда часы являются объектом, значение наблюдателя на самом деле является ссылкой.Когда это свойство изменяется, this.value также изменяется синхронно, поэтому oldValue = = значение Теперь, что касается того, почему автор разработал это таким образом, я думаю, должны быть его причины.
run(cb) {
const value = this.get();
if (value !== this.value) {
const oldValue = this.value;
this.value = value;
cb.call(this.vm, value, oldValue);
}
}
Восемь, компиляция Vue 2+ уже использовал VNode, и эта часть подробно не изучалась, поэтому я написал здесь простой компилятор, который не имеет ничего общего с исходным кодом. Он в основном использует DocumentFragment и замыкание.Заинтересованная детская обувь может пойти на этот склад для проверки.
компоненты vnode будут добавлены...