最近总被问道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)
}
}
对于计算属性,会重复上面的逻辑,直到新的页面渲染完成。