最近总被问道vue的计算属性原理是什么、计算属性是如何作依赖收集的之类的问题,今天用了一天时间好好研究了下源码,把过程基本捋顺了。总的来讲仍是比较简单。vue
先明确一下咱们须要弄清楚的知识点:node
弄清楚以上两点后对computed就会有一个比较全面的了解了。react
首先,须要弄明白响应式属性是怎么实现的,具体我会在其余文章中写,这里了解个大概就能够。在代码中调用new Vue()
的过程实际调用了定义在原型的_init()
,在这个方法里会初始化vue的不少属性,这其中就包括创建响应式属性。它会循环定义在data
中的全部属性值,经过Object.defineProperty
设置每一个属性的访问器属性。express
所以在这个阶段,data
中的属性值在获取或者赋值时就能被拦截。紧接着就是初始化computed属性:数组
这里要给当前页面实例上新增一个computedWatchers
空对象,而后循环computed
上的属性。在vue的文档里关于computed介绍,它既能够是函数,也但是是对象,好比下面这种:dom
new Vue({ computed:{ amount(){ return this.price * this.count } } // 也能够写成下面这种 computed:{ amount:{ get(){ return this.price * this.count }, set(){} } } })
但由于不建议给computed属性赋值,所以比较常见的都是上面那种。因此在上图的源码中,userDef
和getter
都是函数。以后就是判断是不是服务端渲染,不是就实例化一个Watcher
类。那接着来看一下实例化的这个类是什么。源码太长了我就只展现constructor
里的内容。异步
constructor(vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm) } } this.value = this.lazy ? undefined : this.get() }
在这个阶段作了这么几件事情:async
watchers
属性中依次push了每个计算属性的实例。getter
)设置为this.getter
this.value
设置为undefined
到这里为止,计算属性的初始化就完成了,若是给生命周期打了断点,你就会发现这些步骤就是在created
以前完成的。可是到如今,vue只是建立了响应式属性和把每个计算属性用watcher实例化,并无完成计算属性的依赖收集。ide
紧接着,vue会调用原型上的$mount
方法,这里会返回一个函数mountComponent
。函数
这里关注一下这部分代码:
// we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher( vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */ )
在挂载阶段,会再次实例化一次Watcher
类,可是这里和以前实例的类不同的地方在于,他的初始化属性isRenderWatcher
为true。因此区分一下就是,前文所述的循环计算属性时实例化的Watcher
是computedWatcher
,而这里的则是renderWatcher
。除了从字面上能看出他们之间的区别外。在实例化上也有不一样。
// 不一样一 if (isRenderWatcher) { vm._watcher = this } // 不一样二 this.dirty = this.lazy // for lazy watchers // 不一样三 this.value = this.lazy ? undefined : this.get()
renderWatcher
会在页面实例上新增一个_watcher
属性,而且dirty
为false,最重要的是这里会直接调用实例上的方法get()
这块代码就比较重要了,咱们一点一点说。
首先是pushTarget(this)
。pushTarget
方法是定义在Dep
文件里的方法,他的做用是往Dep
类的自有属性target
上赋值,而且往Dep
模块的targetStack
数组push当前的Watcher
实例。所以对于此时的renderWatcher
而言,它的实例被赋值给了Dep
类上的属性。
接下来就是调用当前renderWatcher
实例的getter方法,也就是上面代码中提到的updateComponent
方法。
updateComponent = () => { vm._update(vm._render(), hydrating) }
这里涉及到虚拟dom的部分,我不在这里详说,之后会再分析。所以如今对于页面来讲,就是将vue中定义的全部data,props,methods,computed等挂载在页面上。为了页面正常显示,固然是须要获取值的,上文中所说的为data的每一个属性设置getter访问器属性,这里就能用到。再看下getter的代码:
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 }
Dep.target
上如今是有值的,就是renderWatcher
实例,dep.depend
就能被顺利调用。来看下dep.depend
的代码:
depend() { if (Dep.target) { Dep.target.addDep(this) } }
这里调用了renderWatcher
实例上的addDep
方法:
/** * Add a dependency to this directive. */ 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) } } }
代码看起来可能不是很清晰,实际上这里作了三件事:
renderWatcher
实例的newDepIds
属性不存在当前正在处理的data属性的id,则添加Dep
实例添加到renderWatcher
的newDeps
属性中Dep
实例上的方法dep.addSub
// 添加订阅 addSub(sub: Watcher) { this.subs.push(sub) }
因此第三步就是在作依赖收集的工做。对于这里,就是为每个响应式属性添加了updateComponent
依赖,这样修改响应式属性的值就可以引发页面的从新渲染,也就是vnode
的patch
过程。
相应的,computed
属性也会被渲染在页面上而被调用,和data属性的原理同样,computed
也有访问器属性的设置,在第二张图中,调到的defineComputed
方法:
export function defineComputed(target: any, key: string, userDef: Object | Function) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? (shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get)) : noop sharedPropertyDefinition.set = userDef.set || noop } if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function() { warn(`Computed property "${key}" was assigned to but it has no setter.`, this) } } Object.defineProperty(target, key, sharedPropertyDefinition) }
sharedPropertyDefinition
是一个通用的访问器对象:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
所以当调用计算属性的时候,就是在调用计算属性上绑定的函数。这里在给get
赋值时调用了另外一个函数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 } } }
这部分代码作的事情就颇有意思了,和renderWatcher
调用get
作的相似,watcher.evaluate
方法会间接调用computedWatcher
的get
方法,而后调用计算属性上的函数,由于计算属性会根据不一样的响应式属性而返回值,调用每个响应式属性都会触发getter,所以和计算属性相关的响应式属性的Dep
实例上会订阅计算属性的变化。
说到这,计算属性的依赖收集就作完了。在这以后若是修改了某一个和计算属性绑定的响应式属性,就会触发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() } // #7981: for accessor properties without setter // 用于没有setter的访问器属性 if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() // 注意这里 }
这里会调用dep.notify
:
// 通知 notify() { // stabilize the subscriber list first // 浅拷贝订阅列表 const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order // 关闭异步,则subs不在调度中排序 // 为了保证他们能正确的执行,如今就带他们进行排序 subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
/** * Subscriber interface. * Will be called when a dependency changes. */ update() { debugger /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
对于计算属性,会重复上面的逻辑,直到新的页面渲染完成。