Статья впервые опубликована вgithub Blog.
Эта статья основана наИсходный код Vuev2.x для анализа. Здесь отсортированы только самые важные части исходного кода, а некоторые неосновные части пропущены. Реактивные обновления в основном включаютWatcher,Dep,Observerэти основные категории.
В этой статье в основном разъясняются следующие вопросы, которые легко спутать:
-
Watcher,Dep,ObserverОтношения между этими классами? -
DepсерединаsubsЧто хранится? -
WatcherсерединаdepsЧто хранится? -
Dep.targetЧто это такое и где присваивается значение?
Эта статья начинается непосредственно с нового экземпляра Vue и шаг за шагом раскрывает принцип адаптивности Vue, предполагая следующий простой код Vue:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
watch: {
counter: function(val, oldVal) {
console.log('counter changed...')
}
}
})
1. Инициализация экземпляра Vue
из ВьюЖизненный циклИзвестно, что первыйinitОперация инициализации, эта часть кода находится вinstance/init.jsсередина.
src/core/instance/init.js
initLifecycle(vm) // vm生命周期相关变量初始化操作
initEvents(vm) // vm事件相关初始化
initRender(vm) // 模板解析相关初始化
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子函数
initInjections(vm) // resolve injections before data/props
initState(vm) // vm状态初始化(重点在这里)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用created钩子函数
Вышеупомянутый источникinitState(vm)Это фокус исследования, который реализуетсяprops,methods,data,computed,watchоперация инициализации. Здесь, основываясь на приведенном выше примере, сосредоточьтесь наdataа такжеwatch, Местоположение источникаinstance/state.js
src/core/instance/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) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 对添加的watch进行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2. initData
Экземпляр Vue реализует для каждого из своих данныхgetter/setterметод, который является основой для реализации отзывчивости. оgetter/setterвидимыйMDN web docs. Проще говоря, он принимает значениеthis.counterПри можно настроить некоторые операции, а затем вернуть значение счетчика; при изменении значенияthis.counter = 10При установке значения вы также можете настроить некоторые операции при установке значения.initData(vm)Реализация находится в исходном кодеinstance/state.js.
src/core/instance/state.js
while (i--) {
...
// 这里将data,props,methods上的数据全部代理到vue实例上
// 使得vm.counter可以直接访问
}
// 这里略过上面的代码,直接看最核心的observe方法
// observe data
observe(data, true /* asRootData */)
здесьobserve()Метод превращает данные в наблюдаемые, почему они наблюдаемые? в основном реализованоgetter/setterметод, пустьWatcherМожно наблюдать изменения этих данных. Посмотрите нижеobserveреализация.
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // 重点在这里,响应式的核心所在
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Просто сосредоточься здесьnew Observer(value), который является ядром метода, путемObserverкласс будет вьюdataстать отзывчивым. Согласно нашему примеру, в это время параметрvalueЗначение{ counter: 1 }. Рассмотрим подробно нижеObserverДобрый.
3. Observer
Сначала посмотрите на конструктор этого класса,new Observer(value)Конструктор выполняется первым. В примечании автора говорится, что класс Observer преобразует значение ключа каждого целевого объекта (то есть данные в данных) вgetter/setterформа для сбора и обновления зависимостей через уведомление о зависимостях.
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*/
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value) // 遍历data对象中{counter : 1, ..} 中的每个键值(如counter),设置其setter/getter方法。
}
}
...
}
Самое главное здесьthis.walk(value)метод,this.observeArray(value)Это обработка данных массива для достижения соответствующегоМетод мутации, здесь не рассматривается.
Продолжай читатьwalk()метод, описанный в комментарияхwalk()Что он делает, так это проходит данные каждой настройки в объекте данных и преобразует их вsetter/getter.
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
Затем соответствующие данные окончательно преобразуются вgetter/setterпутьdefineReactive()метод. Также из названия метода легко понять, что метод определен как отзывчивый.В сочетании с первоначальным примером вызов здесьdefineReactive(...)как показано на рисунке:
Исходный код выглядит следующим образом:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// dep 为当前数据的依赖实例
// dep 维护着一个subs列表,保存依赖与当前数据(此时是当前数据是counter)的观察者(或者叫订阅者)。观察者即是Watcher实例。
const dep = new Dep() ---------------(1)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val)
// 定义getter与setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 这里在获取值之前先进行依赖收集,如果Dep.target有值的话。
if (Dep.target) { -----------------(2)
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 依赖收集完后返回值
return value
},
...
}
Первый взглядgetterметод, есть два наиболее важных аспекта этого метода.
- объявить по одному для каждых данных
depобъект экземпляра, затемdepНа замыкание ссылаются соответствующие данные. Например, каждый разcounterКогда значение извлекается или изменяется, к его экземпляру dep можно получить доступ, и он не исчезнет. - согласно с
Dep.targetЧтобы определить, следует ли собирать зависимости или общие значения. здесьDep.targetЗадание последует позже, здесь мы впервые узнаем, что есть такое.
тогда посмотри еще разsetterметод, исходный код выглядит следующим образом:
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 = !shallow && observe(newVal)
// 最重要的是这一步,即通过dep实例通知观察者我的数据更新了
dep.notify()
}
На этом инициализация данных экземпляра Vue в основном завершена.Давайте рассмотрим следующий рисунок.initDataпроцесс:
Далее следуетwatchинициализация:
export function initState (vm: Component) {
...
if (opts.data) {
initData(vm) // 对vm的data进行初始化,主要是通过Observer设置对应getter/setter方法
}
// initData(vm) 完成后进行 initWatch(..)
...
// 对添加的watch进行初始化
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
4. initWatch
здесьinitWatch(vm, opts.watch)Это соответствует нашему примеру следующим образом:
initWatchИсходный код выглядит следующим образом:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
// handler 是观察对象的回调函数
// 如例子中counter的回调函数
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
createWatcher(vm, key, handler)основывается на входных параметрахWatcherПример информации, исходный код выглядит следующим образом:
function createWatcher (
vm: Component,
keyOrFn: string | Function,
handler: any,
options?: Object
) {
// 判断是否是对象,是的话提取对象里面的handler方法
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 判断handler是否是字符串,是的话说明是vm实例上的一个方法
// 通过vm[handler]获取该方法
// 如 handler='sayHello', 那么handler = vm.sayHello
if (typeof handler === 'string') {
handler = vm[handler]
}
// 最后调用vm原型链上的$watch(...)方法创建Watcher实例
return vm.$watch(keyOrFn, handler, options)
}
$watchЭто метод, определенный в цепочке прототипов Vue.Исходный код выглядит следующим образом:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// 创建Watcher实例对象
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
// 该方法返回一个函数的引用,直接调用该函数就会调用watcher对象的teardown()方法,从它注册的列表中(subs)删除自己。
return function unwatchFn () {
watcher.teardown()
}
}
После серии инкапсуляций мы, наконец, видим создание экземпляра объекта Watcher. Это будет подробно объяснено нижеWatcherДобрый.
5. Watcher
Согласно нашему примеру,new Watcher(...)Как показано ниже:
сначала выполнитьWatcherСтруктура класса, исходный код выглядит следующим образом, а какой-то код опущен:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
this.cb = cb // 保存传入的回调函数
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = [] // 保存观察数据当前的dep实例对象
this.newDeps = [] // 保存观察数据最新的dep实例对象
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
// 获取观察对象的get方法
// 对于计算属性, expOrFn为函数
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 通过parsePath方法获取观察对象expOrFn的get方法
this.getter = parsePath(expOrFn)
...
}
// 最后通过调用watcher实例的get()方法,
// 该方法是watcher实例关联观察对象的关键之处
this.value = this.lazy
? undefined
: this.get()
}
parsePath(expOrFn)Конкретный метод реализации заключается в следующем:
/**
* Parse simple path.
*/
const bailRE = /[^\w.$]/ // 匹配不符合包含下划线的任意单词数字组合的字符串
export function parsePath (path: string): any {
// 非法字符串直接返回
if (bailRE.test(path)) {
return
}
// 举例子如 'counter'.split('.') --> ['counter']
const segments = path.split('.')
// 这里返回一个函数给this.getter
// 那么this.getter.call(vm, vm),这里vm就是返回函数的入参obj
// 实际上就是调用vm实例的数据,如 vm.counter,这样就触发了counter的getter方法。
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
Это ловко возвращает метод вthis.getter, который:
this.getter = function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
this.getterбудетthis.get()Вызывается в методе для получения значения наблюдаемого объекта и запуска его коллекции зависимостей, вот получениеcounterценность .
Последний шаг конструктора Watcher, вызовthis.get()метод, методисходный кодследующим образом:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
// 该方法实际上是设置Dep.target = this
// 把Dep.target设置为该Watcher实例
// Dep.target是个全局变量,一旦设置了在观察数据中的getter方法就可使用了
pushTarget(this)
let value
const vm = this.vm
try {
// 调用观察数据的getter方法
// 进行依赖收集和取得观察数据的值
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 此时观察数据的依赖已经收集完
// 重置Dep.target=null
popTarget()
// 清除旧的deps
this.cleanupDeps()
}
return value
}
Ключевые шаги были аннотированы в приведенном выше коде.Ниже показано отношение между классами Observer и Watcher.Рисунок по-прежнему описывается нашим примером:
- Красная стрелка: создается экземпляр класса Watcher, вызывающий экземпляр Watcher.
get()метод и наборDep.targetДля текущего экземпляра наблюдателя инициировать наблюдение за объектомgetterметод. - синяя стрелка:
counterобъектgetterметод срабатывает, вызовdep.depend()Выполнить сбор зависимостей и возвратcounterценность . Зависит от собранных результатов:1.counterэкземпляр dep закрытияsubsДобавьте экземпляр наблюдателя w1, который наблюдает за ним.;2. п1depsдобавить объект наблюденияcounterотдел закрытия. - Оранжевая стрелка: когда
counterПосле изменения значения срабатываетsubsНаблюдайте за его выполнением w1 вupdate()метод и, наконец, вызывает функцию обратного вызова cb для w1.
Другие родственные методы в классе Watcher относительно интуитивно понятны и здесь опущены.Подробности см. в исходном коде класса Watcher.
6. Dep
На рисунке выше классы Observer и Watcher связаны с Dep, так что же такое Dep?
Депа можно сравнить с издателем, Наблюдателя с читателями и Наблюдателя с книгами, связанными с Кэйго Хигасино. Например, читатель w1 интересуется «Прогулкой белой ночи» Кейго Хигасино (в нашем примере счетчик). Как только читатель w1 купит книгу Кейго Хигасино, он автоматически зарегистрируется и заполнит информацию w1 в издателе книги (пример отдела). есть последние новости о книге Кейго Хигашино (например, о скидках), w1 будет уведомлен.
Теперь посмотрим на исходный код Dep:
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 保存观察者watcher实例的数组
this.subs = []
}
// 添加观察者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除观察者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 进行依赖收集
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知观察者数据有变化
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Класс Dep относительно прост, и соответствующий метод также очень интуитивно понятен.Самое главное здесь — поддерживать массив, содержащий экземпляр watcher.subs.
7. Резюме
На этом основные три категории изучены, и теперь вы можете в основном ответить на вопросы в начале статьи.
Q1:Watcher,Dep,ObserverОтношения между этими классами?
А1:Watcherнаблюдатель наблюдаетObserverупакованные данные,DepдаWatcherСвязь между данными наблюдения и основной функцией состоит в том, чтобы полагаться на сбор и уведомление об обновлениях.
Q2:DepсерединаsubsЧто хранится?
A2: subsСохраняется экземпляр Watcher-наблюдателя.
Q3:WatcherсерединаdepsЧто хранится?
А3:depsЧто хранится, так это данные наблюдения в закрытииdepпример.
Q4:Dep.targetЧто это такое и где присваивается значение?
А4:Dep.target— это глобальная переменная, которая сохраняет текущий экземпляр наблюдателя, вnew Watcher()Когда назначение сделано, назначением является текущий экземпляр Watcher.
8. Расширение
Вот пример вычисляемого свойства:
var vue = new Vue({
el: "#app",
data: {
counter: 1
},
computed: {
result: function() {
return 'The result is :' + this.counter + 1;
}
}
})
здесьresultЗначение зависит отcounterзначение, черезresultЭто может лучше отражать адаптивные вычисления Vue. Вычисляемые свойства передаются черезinitComputed(vm, opts.computed)После инициализации, следуя отслеживанию исходного кода, вы обнаружите, что также создается экземпляр Watcher:
watchers[key] = new Watcher(
vm, // 当前vue实例
getter || noop, // result对应的方法 function(){ return 'The result is :' + this.counter + 1;}
noop, // noop是定义的一个空方法,这里没有回调函数用noop代替
computedWatcherOptions // { lazy: true }
)
Схема показана ниже:
Это вычисляемое свойствоresultпотому что это зависит отthis.counter, поэтому настройте наблюдателя для наблюденияresultценность . затем черезdefinedComputed(vm, key, userDef)для определения вычисляемых свойств. попасть в расчетresult, он снова сработаетthis.counterизgetterМетод, это делаетresultЗначение зависит отthis.counterценность .
Наконец, это будет вычисляемое свойствоresultопределить этоsetter/getterАтрибуты:Object.defineProperty(target, key, sharedPropertyDefinition). См. исходный код для более подробной информации.