Эта статья - часть серии Читая исходный код Vue.
В этой части мы:
- Разберёмся, что делают рассмотренные миксины
- Поймём процесс инициализации
Итак, мы внутри src/core/instance/index.js
.
import { initMixin } from './init';
import { stateMixin } from './state';
import { renderMixin } from './render';
import { eventsMixin } from './events';
import { lifecycleMixin } from './lifecycle';
import { warn } from '../util/index';
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);
export default Vue;
Для начала, пройдёмся по этим 5 миксинам и разберёмся, что они делают. После перейдём к функции _init
и посмотрим, что происходит, когда мы выполняем строчку var app = new Vue({...})
.
Откроем ./init.js
, проскролим вниз и прочитаем описания.
Этот файл определяет:
- функцию
initMixin()
, она добавляетVue.prototype._init
, вернёмся к этому в следующей секции - функцию
initInternalComponent()
, комментарии говорят, что эта функция может ускорить внутренние механизмы создания компонентов, потому что динамическое склеивание параметров достаточно медленное - функцию
resolveConstructorOptions()
, которая занимается сбором параметров - функцию
resolveModifiedOptions()
, она относится к этому багу. В кратце - это позволяет вам изменять или добавлять параметры при hot-reload. - функцию
dedupe()
, она используется вresolveModifiedOptions
, чтобы избежать повторного вызова хуков жизненного цикла
Откроем ./state,js
, и в этом большом файле поищем statemixin
.
export function stateMixin(Vue: Class<Component>) {
// flow somehow has problems with directly declared definition object
// when using Object.defineProperty, so we have to procedurally build up
// the object here.
const dataDef = {};
dataDef.get = function() {
return this._data;
};
const propsDef = {};
propsDef.get = function() {
return this._props;
};
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function(newData: Object) {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this,
);
};
propsDef.set = function() {
warn(`$props is readonly.`, this);
};
}
Object.defineProperty(Vue.prototype, '$data', dataDef);
Object.defineProperty(Vue.prototype, '$props', propsDef);
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function(
expOrFn: string | Function,
cb: Function,
options?: Object,
): Function {
const vm: Component = this;
options = options || {};
options.user = true;
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
return function unwatchFn() {
watcher.teardown();
};
};
}
Эта функция определяет:
dataDef
и его геттерpropsDef
и его геттер- сеттер для
dataDef
иpropsDef
, если это не продакшн сборка, которые просто выводят два предупреждения в консоль - добавляет
dataDef
вVue.prototype
как$data
- добавляет
propsDef
вVue.prototype
как$props
Vue.prototype.$set
иVue.prototype.$delete
Vue.prototype.$watch
Звучит знакомо? Да, именно отсюда мы получаем $data
, $props
, $set
, $delete
и $watch
. Внимательно прочитайте этот файл, вы можете научиться нескольким приёмам и позже использовать их на собственных проектах.
Обратили внимание на Watcher
? Похоже на важный класс. Вы правы, позже мы объясним Observer
, Dep
и Watcher
. Их взаимодействие обеспечивает синхронизацию данных и представления.
Откройте ./events.js
и найдите eventsMixin
, она слишком длинная, чтобы приводить её здесь, так что прочитайте её самостоятельно.
Эта функция определяет:
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
Должно быть вы не раз пользовались этими вещами, просто прочитайте этот код и вы узнаете как элегантно оперировать событиями.
Откройте lifecycle.js
, проскрольте вниз и найдите lifecycleMixin
.
Эта функция определяет:
Vue.prototype._update()
, Здесь происходит обновление DOM! Мы разберёмся с этим позжеVue.prototype.$forceUpdate()
Vue.prototype.$destroy()
Эм, что это ниже $destoy
? mountComponent
! Мы уже видели это раньше, это $mount
из ядра, который имеет две обёртки.
Продолжаем, мы нашли несколько функций, относящихся к компоненту. Они используются для обновления DOM, можно их пока пропустить.
Откроем ./render.js
, там определяется Vue.prototype._render()
и несколько вспомогательных фукнций. Они так же появятся в следующих частях, про себя отмечаем, что мы здесь встретили _render
.
Итак, мы разобрались, что делают эти миксины. Они просто добавляют некоторые методы в Vue.prototype
.
Здесь важный момент - как разделить и огранизовать кучу функций. На сколько частей вы бы разбили на месте автора? В какую часть попала бы каждая фукнция? Подумайте об этом с точки зрения автора, это интересно и полезно.
После разбора статических частей вернёмся назад к ядру.
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
В оффициальной документации есть небольшой пример:
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!',
},
});
Давате мысленно разберём что происходит при выполнении этого кода.
Для начала, мы вызваемl new Vue({...})
, что означает
options = {
el: '#app',
data: {
message: 'Hello Vue!',
},
};
Дальше this._init(options)
. Помните где опделена _init()
? Именно, в ./init.js
,откройте файл и прочитайте код функции.
_init()
делает следующее:
- устанавливает
_uid
- Создаёт стартовую метку для измерения производительности
- устанавливает
_isVue
- устанавливает параметры
- устанавливает
_renderProxy
. Прокси используется при разработке, чтобы показывать вам больше информации об отрисовке - устанавливает
_self
- вызывает пачку фукнций для инициализации
- создаёт конечную метку для измерения производительности
- вызывает
$mount()
для обновления DOM
Теперь сфокусируемся на функциях инициализации.
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
callHook()
как легко понять, просто вызывает ваши хуки. Дальше мы подробно рассмотрим остальные 6 функций.
Она расположена в файле ./lifecycle
.
Эта функция соединяет компонент с его родетелем, инициализирует некоторые пемеренные, котороые используются в методах жизненного цикла.
Расположена в ./events.js
.
Эта фукнция инициализирует переменные и обновляет их с учётом родительских подпискок.
Расположена в ./render.js
.
Эта функция инициализируем _vnode
, _staticTrees
и некоторые другие переменные и методы.
Тут мы встречаем VNode
в первый раз.
Что такое VNode? Эта штука используется для построение VDom (virtual DOM). VNode и VDom соотносятся с реальными Node и DOM. Vue использует эти две сущности для обеспечения высокой производительности.
Когда данные изменяются, Vue должна обновить страницу. Самый простой способ - обновить все страницу. Но это дорого обходится браузеру и большая часть усилий тратится впустую. Обычно вы просто обновляете несколько свойств, почему бы просто не обновить те части, которые должны измениться? Поэтому Vue добавляет слой VNode и VDom между данными и представлением, который реализует алгоритм вычисления оптимальных измненений DOM и применяет его с странице.
Мы ещё поговорим о ренедеринге и обновлении позже.
Расположена в ./inject.js
.
Эта функция короткая и простая, она просто находит все что определяется параметрами и добавляет это в ваш компонент.
Постойте, что это? Это defineProperty
? Нет, это defineReactive
. Слово reactive
должно напомнить вам кое о чем. Vue может обновлять представление автоматически при изменении данных, возможжно мы можем найти что-то подходящее внутри этой функции. Давайте попробуем.
Откроем ../observer/index.js
и найдём defineReactive
.
В этой фукнции определяется const dep = new Dep()
, делаются кое-какие проверки, а потом экспортируется геттер и сеттер..
let childOb = observe(val); // <-- ВАЖНО
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend(); // <-- ВАЖНО
if (childOb) {
childOb.dep.depend(); // <-- ВАЖНО
}
if (Array.isArray(value)) {
dependArray(value);
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = observe(newVal); // <-- ВАЖНО
dep.notify(); // <-- ВАЖНО
},
});
Дальше она определяет childOb = observe(val)
и устанавливает новое свойство в компонент.
Я поменил ключевые места комментариями. Даже если вы не читали соответствующий код, вы можете сказать как Vue обновляет представление при изменении данных. Она просто оборачивает значение с помощью геттера и серттера, внутри которого создаёт зависимости и отправляет уведомления об изменении.
Функция defineReactive
используется во многих местах, не только в initInjections
, мы ещё поговорим про Observer
, Dep
и Watcher
в следующих частях, пока даватей вернёмся к нашей инициализации.
Расположена в ./state.js
.
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch) initWatch(vm, opts.watch);
}
Снова старые знакомые. Тут мы получаем наши свойства, методы, данные, вычисляемые свойства и наблюдатели. Давайте пройдёмся по ним.
Делает некоторые проверки и использует defineReactive
чтобы обернуть свойства и засунуть их в копонент.
Просто записывает методы в компонент.
Снова проверки, но тут исползуется proxy
для установки данных. Поищете и почитайте proxy
, и вы узнаете, что он просто пробрасывает обращения к this.name
в this._data['name']
.
Наконец эта фукнция вызывает observe(data, true) /* asRootData */
. Мы будем рассматривать Observer дальше, тут я просто хочу пояснить вот это true
. Какждый наблюдаемый объект имеет свойство с названием vmCount
, которое означает сколько компонентов используют этот объект как источник данных. Если вы вызываете observe
с true
- он выполнит vmCall++
. Значение по-умолчанию для этого поля - 0
.
Возможно это свойство будет использовано в дальнейших операциях, но поискав по проекту, я обнаружил, что Vue использует его только чтобы пометить источник данных. Если obj && obj.vmCount
- этот объект используется для чтения данных из него.
Эта функция прежде всего экспортирует фукнцию, которую мы используем как геттер. После она создаёт Watcher (наблюдатель) с этим геттером и сохраняет его в массиве watchers
. В конце она вызывает defineComputed
, чтобы записать вычисляемое свойство в компонент.
По названию вы можете догадаться, что далают Watcher'ы, но мы их оставим на будущее.
Эта фукнция создаёт наблюдатель за каждым входным параметром, используя createWatcher()
. createWatcher()
вызывает vm.$watch()
, который определён внутри stateMixin
. Проскролив вниз чтобы найти это. vm.$watch()
создаст Watcher и ... Что? Она может вызывать createWatcher()
снова. Что за чертовщина?
Читаем внимательно, если cb
это обычный объект, $watch()
вызовет createWatcher()
. А внутри createWatcher()
она извлечёт параметры и обработчик если обработчик (внутри $watch
он называется cb
) это обычный объект. Хорошо, $watch()
просто отбросит его, потому что не собирается делать лишнюю работу.
Расположена в ./inject.js
.
Эта фукнция извлекает провайдеры из параметров и вызывает их на компоненте.
ОБратили внимание на комментарии после initInjections
и initProvider
? В них говорится:
initInjections(vm); // Разрешить внедрённые зависимости до data/props
initState(vm);
initProvide(vm); // Разрешить провайдер после data/props
Почему в таком порядке? Я позже отвечу на этот вопрос.
Вот и все. Сложно запомнить весь процесс инициализации? Вот картинка специально для вас:
Эта часть немного длинная и в ней много деталей. Процесс инициализации это основа для последующих частей, так что убедитесь, что вы поняли все написанное.
Я не собираюсь рассказывать вам все, так что советую вам перечитать весь процесс инициализации снова и заглянуть в реализацию незнакомых фукнций, чтобы знать как они работают.
Эта часть рассказала про процесс инициализации. После инициализации данные могут изменяться, представляения будут синхронизированы с данными. Как Vue реализует процесс обновления? В следующей части мы это разберём.
Читать следующую часть: [Изменяемые данные - Observer, Dep и Watcher.
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
Почему именно в таком порядке? Я позже отвечу.
Подсказка: подумайте с другой стороны - что если изменить порядок этих строчек?