Vue数据绑定简析
栏目: JavaScript · 发布时间: 5年前
内容简介:作为MVVM框架的一种,Vue最为人津津乐道的当是数据与视图的绑定,将直接操作DOM节点变为修改在阅读源码时,因为文件繁多,引用复杂往往使我们不容易抓住重点,这里我们需要找到一个入口文件,从为了方便阅读,我们去除了
作为MVVM框架的一种,Vue最为人津津乐道的当是数据与视图的绑定,将直接操作DOM节点变为修改 data
数据,利用 Virtual Dom
来 Diff
对比新旧视图,从而实现更新。不仅如此,还可以通过 Vue.prototype.$watch
来监听 data
的变化并执行回调函数,实现自定义的逻辑。虽然日常的编码运用已经驾轻就熟,但未曾去深究技术背后的实现原理。作为一个好学的程序员,知其然更要知其所以然,本文将从源码的角度来对Vue响应式数据中的观察者模式进行简析。
初始化 Vue
实例
在阅读源码时,因为文件繁多,引用复杂往往使我们不容易抓住重点,这里我们需要找到一个入口文件,从 Vue
构造函数开始,抛开其他无关因素,一步步理解响应式数据的实现原理。首先我们找到 Vue
构造函数:
// src/core/instance/index.js 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) } 复制代码
// src/core/instance/init.js Vue.prototype._init = function (options) { ... // a flag to avoid this being observed vm._isVue = true // merge options // 初始化vm实例的$options if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } ... initLifecycle(vm) // 梳理实例的parent、root、children和refs,并初始化一些与生命周期相关的实例属性 initEvents(vm) // 初始化实例的listeners initRender(vm) // 初始化插槽,绑定createElement函数的vm实例 callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) // 挂载组件到节点 } } 复制代码
为了方便阅读,我们去除了 flow
类型检查和部分无关代码。可以看到,在实例化Vue组件时,会调用 Vue.prototype._init
,而在方法内部,数据的初始化操作主要在 initState
(这里的 initInjections
和 initProvide
与 initProps
类似,在理解了 initState
原理后自然明白),因此我们重点来关注 initState
。
// src/core/instance/state.js export function initState (vm) { 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 && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } 复制代码
首先初始化了一个 _watchers
数组,用来存放 watcher
,之后根据实例的 vm.$options
,相继调用 initProps
、 initMethods
、 initData
、 initComputed
和 initWatch
方法。
initProps
function initProps (vm, propsOptions) { const propsData = vm.$options.propsData || {} const props = vm._props = {} // cache prop keys so that future props updates can iterate using Array // instead of dynamic object key enumeration. const keys = vm.$options._propKeys = [] const isRoot = !vm.$parent // root instance props should be converted if (!isRoot) { toggleObserving(false) } for (const key in propsOptions) { keys.push(key) const value = validateProp(key, propsOptions, propsData, vm) ... defineReactive(props, key, value) if (!(key in vm)) { proxy(vm, '_props', key) } } toggleObserving(true) } 复制代码
在这里, vm.$options.propsData
是通过父组件传给子组件实例的数据对象,如 <my-element :item="false"></my-element>
中的 {item: false}
,然后初始化 vm._props
和 vm.$options._propKeys
分别用来保存实例的 props
数据和 keys
,因为子组件中使用的是通过 proxy
引用的 _props
里的数据,而不是父组件传递的 propsData
,所以这里缓存了 _propKeys
,用来 updateChildComponent
时能更新 vm._props
。接着根据 isRoot
是否是根组件来判断是否需要调用 toggleObserving(false)
,这是一个全局的开关,来控制是否需要给对象添加 __ob__
属性。这个相信大家都不陌生,一般的组件的 data
等数据都包含这个属性,这里先不深究,等之后和 defineReactive
时一起讲解。因为 props
是通过父传给子的数据,在父元素 initState
时已经把 __ob__
添加上了,所以在不是实例化根组件时关闭了这个全局开关,待调用结束前在通过 toggleObserving(true)
开启。
之后是一个 for
循环,根据组件中定义的 propsOptions
对象来设置 vm._props
,这里的 propsOptions
就是我们常写的
export default { ... props: { item: { type: Object, default: () => ({}) } } } 复制代码
循环体内,首先
const value = validateProp(key, propsOptions, propsData, vm) 复制代码
validateProp
方法主要是校验数据是否符合我们定义的 type
,以及在 propsData
里未找到 key
时,获取默认值并在对象上定义 __ob__
,最后返回相应的值,在这里不做展开。
这里我们先跳过 defineReactive
,看最后
if (!(key in vm)) { proxy(vm, '_props', key) } 复制代码
其中 proxy
方法:
function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } 复制代码
在 vm
不存在 key
属性时,通过 Object.defineProperty
使得我们能通过 vm[key]
访问到 vm._props[key]
。
defineReactive
在 initProps
中,我们了解到其首先根据用户定义的 vm.$options.props
对象,通过对父组件设置的传值对象 vm.$options.propsData
进行数据校验,返回有效值并保存到 vm._props
,同时保存相应的 key
到 vm.$options._propKeys
以便进行子组件的 props
数据更新,最后利用 getter/setter
存取器属性,将 vm[key]
指向对 vm._props[key]
的操作。但其中跳过了最重要的 defineReactive
,现在我们将通过阅读 defineReactive
源码,了解响应式数据背后的实现原理。
// src/core/observer/index.js export function defineReactive ( obj, key, val, customSetter, shallow ) { const dep = new Dep() 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 if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) ... } 复制代码
首先 const dep = new Dep()
实例化了一个 dep
,在这里利用闭包来定义一个依赖项,用以与特定的 key
相对应。因为其通过 Object.defineProperty
重写 target[key]
的 getter/setter
来实现数据的响应式,因此需要先判断对象 key
的 configurable
属性。接着
if ((!getter || setter) && arguments.length === 2) { val = obj[key] } 复制代码
arguments.length === 2
意味着调用 defineReactive
时未传递 val
值,此时 val
为 undefined
,而 !getter || setter
判断条件则表示如果在 property
存在 getter
且不存在 setter
的情况下,不会获取 key
的数据对象,此时 val
为 undefined
,之后调用 observe
时将不对其进行深度观察。正如之后的 setter
访问器中的:
if (getter && !setter) return 复制代码
此时数据将是只读状态,既然是只读状态,则不存在数据修改问题,继而无须深度观察数据以便在数据变化时调用观察者注册的方法。
Observe
在 defineReactive
里,我们先获取了 target[key]
的 descriptor
,并缓存了对应的 getter
和 setter
,之后根据判断选择是否获取 target[key]
对应的 val
,接着是
let childOb = !shallow && observe(val) 复制代码
根据 shallow
标志来确定是否调用 observe
,我们来看下 observe
函数:
// src/core/observer/index.js export function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } let ob if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } 复制代码
首先判断需要观察的数据是否为对象以便通过 Object.defineProperty
定义 __ob__
属性,同时需要 value
不属于 VNode
的实例( VNode
实例通过 Diff
补丁算法来实现实例对比并更新)。接着判断 value
是否已有 __ob__
,如果没有则进行后续判断:
-
shouldObserve
:全局开关标志,通过toggleObserving
来修改。 -
!isServerRendering()
:判断是否服务端渲染。 -
(Array.isArray(value) || isPlainObject(value))
:数组和纯对象时才允许添加__ob__
进行观察。 -
Object.isExtensible(value)
:判断value
是否可扩展。 -
!value._isVue
:避免Vue
实例被观察。
满足以上五个条件时,才会调用 ob = new Observer(value)
,接下来我们要看下 Observer
类里做了哪些工作
// src/core/observer/index.js export class Observer { constructor (value) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 复制代码
构造函数里初始化了 value
、 dep
和 vmCount
三个属性,为 this.value
添加 __ob__
对象并指向自己,即 value.__ob__.value === value
,这样就可以通过 value
或 __ob__
对象取到 dep
和 value
。 vmCount
的作用主要是用来区分是否为 Vue
实例的根 data
, dep
的作用这里先不介绍,待与 getter/setter
里的 dep
一起解释。
接着根据 value
是数组还是纯对象来分别调用相应的方法,对 value
进行递归操作。当 value
为纯对象时,调用 walk
方法,递归调用 defineReactive
。当 value
是数组类型时,首先判断是否有 __proto__
,有就使用 __proto__
实现原型链继承,否则用 Object.defineProperty
实现拷贝继承。其中继承的基类 arrayMethods
来自 src/core/observer/array.js
:
// src/core/observer/array.js const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) }) 复制代码
这里为什么要对数组的实例方法进行重写呢?代码里的 methodsToPatch
这些方法并不会返回新的数组,导致无法触发 setter
,因而不会调用观察者的方法。所以重写了这些变异方法,使得在调用的时候,利用 observeArray
对新插入的数组元素添加 __ob__
,并能够通过 ob.dep.notify
手动通知对应的被观察者执行注册的方法,实现数组元素的响应式。
if (asRootData && ob) { ob.vmCount++ } 复制代码
最后添加这个 if
判断,在 Vue
实例的根 data
对象上,执行 ob.vmCount++
,这里主要为了后面根据 ob.vmCount
来区分是否为根数据,从而在其上执行 Vue.set
和 Vue.delete
。
getter/setter
在对 val
进行递归操作后(假如需要的话),将 obj[key]
的数据对象封装成了一个被观察者,使得能够被观察者观察,并在需要的时候调用观察者的方法。这里通过 Object.defineProperty
重写了 obj[key]
的访问器属性,对 getter/setter
操作做了拦截处理, defineReactive
剩余的代码具体如下:
... 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) { ... childOb = !shallow && observe(newVal) dep.notify() } }) 复制代码
首先在 getter
调用时,判断 Dep.target
是否存在,若存在则调用 dep.depend
。我们先不深究 Dep.target
,只当它是一个观察者,比如我们常用的某个计算属性,调用 dep.depend
会将 dep
当做计算属性的依赖项存入其依赖列表,并把这个计算属性注册到这个 dep
。这里为什么需要互相引用呢?这是因为一个 target[key]
可以充当多个观察者的依赖项,同时一个观察者可以有多个依赖项,他们之间属于多对多的关系。这样当某个依赖项改变时,我们可以根据 dep
里维护的观察者,调用他们的注册方法。现在我们回过头来看 Dep
:
// src/core/observer/dep.js export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ 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() } } } 复制代码
构造函数里,首先添加一个自增的 uid
用以做 dep
实例的唯一性标志,接着初始化一个观察者列表 subs
,并定义了添加观察者方法 addSub
和移除观察者方法 removeSub
。可以看到其在 getter
中调用的 depend
会将当前这个 dep
实例添加到观察者的依赖项,在 setter
里调用的 notify
会执行各个观察者注册的 update
方法, Dep.target.addDep
这个方法将在之后的 Watcher
里进行解释。简单来说就是会在 key
的 getter
触发时进行 dep
依赖收集到 watcher
并将 Dep.target
添加到当前 dep
的观察者列表,这样在 key
的 setter
触发时,能够通过观察者列表,执行观察者的 update
方法。
当然,在 getter
中还有如下几行代码:
if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } 复制代码
这里可能会有疑惑,既然已经调用了 dep.depend
,为什么还要调用 childOb.dep.depend
?两个 dep
之间又有什么关系呢?
其实这两个 dep
的分工是不同的。对于数据的增、删,利用 childOb.dep.notify
来调用观察者方法,而对于数据的修改,则使用的 dep.notify
,这是因为 setter
访问器无法监听到对象数据的添加和删除。举个例子:
const data = { arr: [{ value: 1 }], } data.a = 1; // 无法触发setter data.arr[1] = {value: 2}; // 无法触发setter data.arr.push({value: 3}); // 无法触发setter data.arr = [{value: 4}]; // 可以触发setter 复制代码
还记得 Observer
构造函数里针对数组类型 value
的响应式转换吗?通过重写 value
原型链,使得对于新插入的数据:
if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() 复制代码
将其转换为响应式数据,并通过 ob.dep.notify
来调用观察者的方法,而这里的观察者列表就是通过上述的 childOb.dep.depend
来收集的。同样的,为了实现对象新增数据的响应式,我们需要提供相应的 hack
方法,而这就是我们常用的 Vue.set/Vue.delete
。
// src/core/observer/index.js export function set (target: Array<any> | Object, key: any, val: any): any { ... if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) ob.dep.notify() return val } 复制代码
- 判断
value
是否为数组,如果是,直接调用已经hack
过的splice
即可。 - 是否已存在
key
,有的话说明已经是响应式了,直接修改即可。 - 接着判断
target.__ob__
是否存在,如果没有说明该对象无须深度观察,设置返回当前的值。 - 最后,通过
defineReactive
来设置新增的key
,并调用ob.dep.notify
通知到观察者。
现在我们了解了 childOb.dep.depend()
是为了将当前 watcher
收集到 childOb.dep
,以便在增、删数据时能通知到 watcher
。而在 childOb.dep.depend()
之后还有:
if (Array.isArray(value)) { dependArray(value) } 复制代码
/** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */ function dependArray (value: Array<any>) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } } 复制代码
在触发 target[key]
的 getter
时,如果 value
的类型为数组,则递归将其每个元素都调用 __ob__.dep.depend
,这是因为无法拦截数组元素的 getter
,所以将当前 watcher
收集到数组下的所有 __ob__.dep
,这样当其中一个元素触发增、删操作时能通知到观察者。比如:
const data = { list: [[{value: 0}]], }; data.list[0].push({value: 1}); 复制代码
这样在 data.list[0].__ob__.notify
时,才能通知到 watcher
。
target[key]
的 getter
主要作用:
- 将
Dep.target
收集到闭包中dep
的观察者列表,以便在target[key]
的setter
修改数据时通知观察者 - 根据情况对数据进行遍历添加
__ob__
,将Dep.target
收集到childOb.dep
的观察者列表,以便在增加/删除数据时能通知到观察者 - 通过
dependArray
将数组型的value
递归进行观察者收集,在数组元素发生增、删、改时能通知到观察者
target[key]
的 setter
主要作用是对新数据进行观察,并通过闭包保存到 childOb
变量供 getter
使用,同时调用 dep.notify
通知观察者,在此就不再展开。
Watcher
在前面的篇幅中,我们主要介绍了 defineReactive
来定义响应式数据:通过闭包保存 dep
和 childOb
,在 getter
时来进行观察者的收集,使得在数据修改时能触发 dep.notify
或 childOb.dep.notify
来调用观察者的方法进行更新。但具体是如何进行 watcher
收集的却未做过多解释,现在我们将通过阅读 Watcher
来了解观察者背后的逻辑。
function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) const isSSR = isServerRendering() for (const key in computed) { const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (!isSSR) { // create internal watcher for the computed property. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } ... } } 复制代码
这是 Vue
计算属性的初始化操作,去掉了一部分不影响的代码。首先初始化对象 vm._computedWatchers
用以存储所有的计算属性, isSSR
用以判断是否为服务端渲染。再根据我们编写的 computed
键值对循环遍历,如果不是服务端渲染,则为每个计算属性实例化一个 Watcher
,并以键值对的形式保存到 vm._computedWatchers
对象,接下来我们主要看下 Watcher
这个类。
Watcher
的构造函数
构造函数接受5个参数,其中当前 Vue
实例 vm
、求值表达式 expOrFn
(支持 Function
或者 String
,计算属性中一般为 Function
),回调函数 cb
这三个为必传参数。设置 this.vm = vm
用以后续绑定 this.getter
的执行环境,并将 this
推入 vm._watchers
( vm._watchers
用以维护实例 vm
中所有的观察者),另外根据是否为渲染观察者来赋值 vm._watcher = this
(常用的 render
即为渲染观察者)。接着根据 options
进行一系列的初始化操作。其中有几个属性:
-
this.lazy
:设置是否懒求值,这样能保证有多个被观察者发生变化时,能只调用求值一次。 -
this.dirty
:配合this.lazy
,用以标记当前观察者是否需要重新求值。 -
this.deps
、this.newDeps
、this.depIds
、this.newDepIds
:用以维护被观察对象的列表。 -
this.getter
:求值函数。 -
this.value
:求值函数返回的值,即为计算属性中的值。
Watcher
的求值
因为计算属性是惰性求值,所以我们继续看 initComputed
循环体:
if (!(key in vm)) { defineComputed(vm, key, userDef) } 复制代码
defineComputed
主要将 userDef
转化为 getter/setter
访问器,并通过 Object.defineProperty
将 key
设置到 vm
上,使得我们能通过 this[key]
直接访问到计算属性。接下来我们主要看下 userDef
转为 getter
中的 createComputedGetter
函数:
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } } 复制代码
利用闭包保存计算属性的 key
,在 getter
触发时,首先通过 this._computedWatchers[key]
获取到之前保存的 watcher
,如果 watcher.dirty
为 true
时调用 watcher.evaluate
(执行 this.get()
求值操作,并将当前 watcher
的 dirty
标记为 false
),我们主要看下 get
操作:
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { ... } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } 复制代码
可以看到,求值时先执行 pushTarget(this)
,通过查阅 src/core/observer/dep.js
,我们可以看到:
Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] } 复制代码
pushTarget
主要是把 watcher
实例进栈,并赋值给 Dep.target
,而 popTarget
则相反,把 watcher
实例出栈,并将栈顶赋值给 Dep.target
。 Dep.target
这个我们之前在 getter
里见到过,其实就是当前正在求值的观察者。这里在求值前将 Dep.target
设置为 watcher
,使得在求值过程中获取数据时触发 getter
访问器,从而调用 dep.depend
,继而执行 watcher
的 addDep
操作:
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) } } } 复制代码
先判断 newDepIds
是否包含 dep.id
,没有则说明尚未添加过这个 dep
,此时将 dep
和 dep.id
分别加到 newDepIds
和 newDeps
。如果 depIds
不包含 dep.id
,则说明之前未添加过此 dep
,因为是双向添加的(将 dep
添加到 watcher
的同时也需要将 watcher
收集到 dep
),所以需要调用 dep.addSub
,将当前 watcher
添加到新的 dep
的观察者队列。
if (this.deep) { traverse(value) } 复制代码
再接着根据 this.deep
来调用 traverse
。 traverse
的作用主要是递归遍历触发 value
的 getter
,调用所有元素的 dep.depend()
并过滤重复收集的 dep
。最后调用 popTarget()
将当前 watcher
移出栈,并执行 cleanupDeps
:
cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } ... } 复制代码
遍历 this.deps
,如果在 newDepIds
中不存在 dep.id
,则说明新的依赖里不包含当前 dep
,需要到 dep
的观察者列表里去移除当前这个 watcher
,之后便是 depIds
和 newDepIds
、 deps
和 newDeps
的值交换,并清空 newDepIds
和 newDeps
。到此完成了对 watcher
的求值操作,同时更新了新的依赖,最后返回 value
即可。
回到 createComputedGetter
接着看:
if (Dep.target) { watcher.depend() } 复制代码
当执行计算属性的 getter
时,有可能表达式中还有别的计算属性依赖,此时我们需要执行 watcher.depend
将当前 watcher
的 deps
添加到 Dep.target
即可。最后返回求得的 watcher.value
即可。
总的来说我们从 this[key]
触发 watcher
的 get
函数,将当前 watcher
入栈,通过求值表达式将所需要的依赖 dep
收集到 newDepIds
和 newDeps
,并将 watcher
添加到对应 dep
的观察者列表,最后清除无效 dep
并返回求值结果,这样就完成了依赖关系的收集。
Watcher
的更新
以上我们了解了 watcher
的依赖收集和 dep
的观察者收集的基本原理,接下来我们了解下 dep
的数据更新时如何通知 watcher
进行 update
操作。
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.notify
时,我们将 this.subs
拷贝出来,防止在 watcher
的 get
时候 subs
发生更新,之后调用 update
方法:
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } 复制代码
- 如果是
lazy
,则将其标记为this.dirty = true
,使得在this[key]
的getter
触发时进行watcher.evaluate
调用计算。 - 如果是
sync
同步操作,则执行this.run
,调用this.get
求值和执行回调函数cb
。 - 否则执行
queueWatcher
,选择合适的位置,将watcher
加入到队列去执行即可,因为和响应式数据无关,故不再展开。
小结
因为篇幅有限,只对数据绑定的基本原理做了基本的介绍,在这画了一张简单的流程图来帮助理解 Vue
的响应式数据,其中省略了一些 VNode
等不影响理解的逻辑及边界条件,尽可能简化地让流程更加直观:
最后,本着学习的心态,在写作的过程中也零零碎碎的查阅了很多资料,其中难免出现纰漏以及未覆盖到的知识点,如有错误,还请不吝指教。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Cracking the Coding Interview
Gayle Laakmann McDowell / CareerCup / 2015-7-1 / USD 39.95
Cracking the Coding Interview, 6th Edition is here to help you through this process, teaching you what you need to know and enabling you to perform at your very best. I've coached and interviewed hund......一起来看看 《Cracking the Coding Interview》 这本书的介绍吧!