最近总被问道 vue 的计算属性原理是什么、计算属性是如何做依赖收集的之类的问题,今天用了一天时间好好研究了下源码,把过程基本捋顺了。总的来说还是比较简单。
先明确一下我们需要弄清楚的知识点:
- computed 属性如何初始化
- 响应式属性的变化如何引起 computed 的重新计算
弄清楚以上两点后对 computed 就会有一个比较全面的了解了。
首先,需要弄明白响应式属性是怎么实现的,具体我会在其他文章中写,这里了解个大概就可以。在代码中调用 new Vue()
的过程实际调用了定义在原型的 _init()
,在这个方法里会初始化 vue 的很多属性,这其中就包括建立响应式属性。它会循环定义在data
中的所有属性值,通过 Object.defineProperty
设置每个属性的访问器属性。
因此在这个阶段,data
中的属性值在获取或者赋值时就能被拦截。紧接着就是初始化 computed 属性:
这里要给当前页面实例上新增一个 computedWatchers
空对象,然后循环 computed
上的属性。在 vue 的文档里关于 computed 介绍,它既可以是函数,也可是是对象,比如下面这种:
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()}
在这个阶段做了这么几件事情:
- 向页面实例的
watchers
属性中依次 push 了每一个计算属性的实例。 - 将实例化类时传入的第二个参数(也就是上文提及的
getter
)设置为this.getter
-
this.value
设置为undefined
到这里为止,计算属性的初始化就完成了,如果给生命周期打了断点,你就会发现这些步骤就是在 created
之前完成的。但是到现在,vue 只是创建了响应式属性和把每一个计算属性用 watcher 实例化,并没有完成计算属性的依赖收集。
紧接着,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,则添加 - 将当前 data 属性的
Dep
实例添加到renderWatcher
的newDeps
属性中 - 调用当前 data 属性的
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)
}
}
对于计算属性,会重复上面的逻辑,直到新的页面渲染完成。