vue源码分析系列之响应式数据(三)
前言
上一节着重讲述了initData
中的代码,以及数据是如何从data中到视图层的,以及data修改后如何作用于视图。这一节主要记录initComputed
中的内容。
正文
前情回顾
在demo示例中,我们定义了一个计算属性。
computed:{ total(){ return this.a + this.b } }
本章节我们继续探究这个计算属性的相关流程。
initComputed
// initComputed(vm, opts.computed) function initComputed (vm: Component, computed: Object) { // 定义计算属性相关的watchers. const watchers = vm._computedWatchers = Object.create(null) // 是否是服务端渲染,这里赞不考虑。 const isSSR = isServerRendering() for (const key in computed) { // 获得用户定义的计算属性中的item,通常是一个方法 // 在示例程序中,仅有一个key为total的计算a+b的方法。 const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) } if (!isSSR) { // create internal watcher for the computed property. // 为计算属性创建一个内部的watcher。 // 其中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不用计算。 // 只有在需要的情况下才计算,这里主要是在后期页面渲染中,生成虚拟dom的时候才会计算。 // 这时候new Watcher只是走一遍watcher的构造函数,其内部value由于 // lazy为true,先设置为了undefined.同时内部的dirty = lazy; watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions // 上文定义过,值为{lazy: true} ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。 if (!(key in vm)) { // 将计算属性定义到实例中。 defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } }
defineComputed
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } // defineComputed(vm, key, userDef) export function defineComputed ( target: any, key: string, userDef: Object | Function ) { // 是否需要缓存。即非服务端渲染需要缓存。 // 由于本案例用的demo非服务端渲染,这里结果是true const shouldCache = !isServerRendering() if (typeof userDef === 'function') { // userDef = total() {...} sharedPropertyDefinition.get = shouldCache // 根据key创建计算属性的getter ? createComputedGetter(key) : userDef // 计算属性是只读的,所以设置setter为noop. sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.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) }
createComputedGetter
// 根据key创建计算属性的getter // createComputedGetter(key) function createComputedGetter (key) { return function computedGetter () { // 非服务端渲染的时候,在上述的initComputed中定义了vm._computedWatchers = {},并根据组件中的设定watchers[key] = new Watcher(..),这里只是根据key取出了当时new的watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。 // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为 // dirty = lazy = true; // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候, // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候 // 我们的total就已经脏了。需要根据最新的a,b计算。 if (watcher.dirty) { // 计算watcher中的值,即value属性. watcher.evaluate() } // 将依赖添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的结果就是返回getter中的值。 return watcher.value } } }
initComputed小结
继initComputed之后,所有组件中的computed都被赋值到了vm实例的属性上,并设置好了getter和setter。在非服务端渲染的情况下,getter会缓存计算结果。并在需要的时候,才计算。setter则是一个什么都不做的函数,预示着计算属性只能被get,不能被set。即只读的。
接下来的问题就是:
- 这个计算属性什么时候会计算,前文
{lazy:true}
预示着当时new Watcher得到的值是undefined。还没开始计算。 - 计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新。
- vue官方文档的缓存计算结果怎么理解。
接下来我们继续剖析后面的代码。解决这里提到的三个问题。
用来生成vnode的render函数
下次再见到这个计算属性total的时候,已是在根据el选项或者template模板中,生成的render函数,render函数上一小节也提到过。长这个样子。
(function anonymous() { with (this) { return _c('div', { attrs: { "id": "demo" } }, [_c('div', [_c('p', [_v("a:" + _s(a))]), _v(" "), _c('p', [_v("b: " + _s(b))]), _v(" "), _c('p', [_v("a+b: " + _s(total))]), _v(" "), _c('button', { on: { "click": addA } }, [_v("a+1")])])]) } } )
这里可以结合一下我们的html,看出一些特点。
<div id="demo"> <div> <p>a:{{a}}</p> <p>b: {{b}}</p> <p>a+b: {{total}}</p> <button @click="addA">a+1</button> </div> </div>
这里使用到计算属性的主要是这一句
_v("a+b: " + _s(total))
那么对于我们来说的关键就是_s(total)
。由于这个函数的with(this)
中,this
被设置为vm实例,所以这里就可以理解为_s(vm.total)
。那么这里就会触发之前定义的sharedPropertyDefinition.get
-> initComputed() -> defineComputed() -> Object.defineProperty(target, key, sharedPropertyDefinition)
也就是createComputedGetter
返回的函数中的内容,也就是:
watcher细说
const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 由于初始化的时候这个dirty为true,所以会进行watcher.evaluate()的计算。 if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } // getter的结果就是返回getter中的值。 return watcher.value }
这里我们看下watcher.evaluate的部分。
// class Watcher内部 /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false }
这里this.get
即得到了value的值,这就是第一个问题的答案。
1.计算属性何时会计算。
即用到的时候会计算,精确的说,就是在计算vnode的时候会用到它,从而计算它。
对于第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?则是在这个this.get
内。
// Dep相关逻辑,Dep Class用来收集依赖某个值的watcher Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { Dep.target = targetStack.pop() } // Watcher class 相关逻辑 get () { // 将当前的watcher推到Dep.target中 pushTarget(this) let value const vm = this.vm try { // 这里的getter实际上就是对应total的函数体, // 而这个函数体内藏有很大的猫腻,接下来我们仔细分析这一段。 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) } popTarget() this.cleanupDeps() } return value }
当代码执行到this.getter.call
,实际上执行的是计算属性的函数,也就是total() { return this.a + this.b}
;当代码执行到this.a时候。就会触发上一节我们所讲的defineReactive
内部的代码。
//// 这里我们以访问this.a为例 export function defineReactive ( obj: Object, // {a:1,b:1} key: string, // 'a' val: any, // 1 customSetter?: ?Function, shallow?: boolean ) { 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 let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // this.a会触发这里的代码。首先获得value, // 由于watcher内部this.get执行total计算属性时候,已经将 // total的watcher设置为Dep.target if (Dep.target) { // 所以这里开始收集依赖。 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, 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.notify() } }) }
上述代码中,this.a
触发了dep.depend()
。我们细看这里的代码。
class Dep { //省略代码... depend () { // 由于这里的Dep.target此时对应的是total的watcher。 // 而这里的this.是指定义this.a时,生成的dep。 // 所以这里是告诉total依赖于this.a if (Dep.target) { // 通过调用addDep.让total的watcher知道total依赖this.a Dep.target.addDep(this) } } } class Watcher { // ...省略代码 addDep (dep: Dep) { // 此时的this是total的watcher const id = dep.id // 防止重复收集 if (!this.newDepIds.has(id)) { // 将依赖的可观察对象记录。 this.newDepIds.add(id) this.newDeps.push(dep) // 如果这个可观察对象没有记录当前watcher, if (!this.depIds.has(id)) { // 则将当前的watcher加入到可观察对象中 // (方便后续a变化后,告知total) dep.addSub(this) } } } }
至此,上述的第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?也有了答案。就是当生成虚拟dom的时候,用到了total,由于得到total值的watcher是脏的,需要计算一次,然后就将Dep.target的watcher设为total相关的watcher。并在watcher内执行了total函数,在函数内部,访问了this.a。this.a的getter中,通过dep.depend()
,将this.a的getter上方的dep,加入到total的watcher.dep中,再通过watcher中的dep.addSub(this)
,将total的watcher加入到了this.a的getter上方中的dep中。至此total知道了它依赖于this.a。this.a也知道了,total需要this.a。
当计算属性的依赖变更时发生了什么
当点击页面按钮的时候,会执行我们案例中绑定的this.a += 1的代码。此时会走
this.a的setter函数。我们看看setter中所做的事情。
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 如果旧值与新值相当,什么都不做。直接返回。 if (newVal === value || (newVal !== newVal && value !== value)) { return } // 无关代码,pass if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // 有定义过setter的话通过setter设置新值 if (setter) { setter.call(obj, newVal) } else { // 否则的话直接设置新值 val = newVal } // 考虑新值是对象的情况。 childOb = !shallow && observe(newVal) // 通知观察了this.a的观察者。 // 这里实际上是有两个观察a的观察者 // 一个是上一篇讲的updateComponent。 // 一个是这节讲的total。 dep.notify() }
这里我们看看dep.notify干了什么
class Dep { // **** 其他代码 notify () { // 这里的subs其实就是上述的两个watcher。 // 分别执行watcher的update const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } class Watcher{ update () { // 第一个watcher,即关于updateComponent的。 // 会执行queueWatcher。也就是会将处理放到等待队列里 // 等待队列中,而第二个watcher由于lazy为true, // 所以只是将watcher标记为dirty。 // 由于队列这个比较复杂,所以单开话题去讲 // 这里我们只需要知道它是一个异步的队列,最后结果就是 // 挨个执行队列中watcher的run方法。 if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } } run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } } }
当触发了依赖更新时候,第一个watcher(关于total的)会将自己的dirty标记为true,第二个则会执行run方法,在其中运行this.get导致updateComponent执行,进而再次计算vnode,这时会再次计算this.total。则会再次触发total的getter,这时候我们再复习一下之前讲过的这个computed的getter:
const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。 // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为 // dirty = lazy = true; // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候, // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候 // 我们的total就已经脏了。需要根据最新的a,b计算。 if (watcher.dirty) { // 计算watcher中的值,即value属性. watcher.evaluate() } // 将依赖添加到watcher中。 if (Dep.target) { watcher.depend() } // getter的结果就是返回getter中的值。 return watcher.value }
至此,computed中total的更新流程也结束了。
所以我们的第3个问题,vue官方文档的缓存计算结果怎么理解?也就有了答案。也就是说计算属性只有其依赖变更的时候才会去计算,依赖不更新的时候,是不会计算的。正文这一小节提到的,total的更新是由于this.a的更新导致其setter被触发,因此通知了其依赖,即total这个watcher。如果total的不依赖于this.a,则total相关的watcher的dirty就不会变为true,也就不会再次计算了。
总结
本章节我们以示例程序探究了计算属性,从initComputed
中,计算属性的初始化到计算属性的变更,对着代码做了进一步的解释。整体流程可以归纳为:
initComputed
定义了相关的计算属性相关的watcher,以及watcher的getter。
在第一次计算vnode的时候顺便执行了计算属性的计算逻辑,顺便收集了依赖。本例中total收集到了依赖a,b;并且a,b也被告知total观察了他们。当a,b任何一个改变时的时候,就会将total相关的watcher.dirty设置为true,下次需要更新界面时,计算属性就会被重新计算。当然,如果没有依赖于total的地方。那么total是不会计算的,例如total根本没被界面或者js代码用到,就不会计算total;如果total所有的依赖没有变更,其dirty为false,则也是无需计算的。
文章链接
- vue源码分析系列
- vue源码分析系列之debug环境搭建
- vue源码分析系列之入口文件分析
- vue源码分析系列之响应式数据(一)
- vue源码分析系列之响应式数据(二)