Vue源码学习7-响应式原理-computed
前言
computed(计算属性)是Vue中比较好玩的一个东西,他在定义的时候和 data或者props 完全不同,但它的使用方式又和 data/props 没啥区别。
它的出现是其实 Vue 为了避免开发者在模板中写太多的复杂运算逻辑,将这些逻辑都转移都js代码中
--以下所有代码来自Vue 2.6.11 版本
--以下的vm都代指vue实例
贴源码
通过第一篇文章可以了解到,initComputed 是发生在 initState 逻辑中,且初始化的顺序是排在 props / methods / data 之后,watch 之前的,为啥在watch之前?因为watch除了可以监听 data/props 的数据,还可以监听 computed,要知道 计算属性 背后其实是一个函数,它是如何做到像 data/props 那样使用的,看看源码就知道了。
这是一段完整的代码,来源 src/core/instance/state.js,它是从 initState 进来的,第一个入参是 vm,第二个入参就是我们自己在 computed 中写的计算函数了,贴个实例代码:
data:{ person:'Tifa' }, computed:{ person1(){ return 'my name is ' + this.person; } }
1:看到源码,直接看 for循环,一个简单的遍历,忽略一些逻辑,走到 new Watcher ,重点来了,这里的 Watcher 入参和前面文章讲的那个 render-watcher 的入参是不一样的(具体有哪些差异私下简单看看就行了)。
2:getter,求值函数,Vue的 computed 提供了两种写法(具体怎么去看文档 )总之 ,这个 getter 就用来取值的
3:computedWatcherOptions 是一个公共的对象,和 initComputed 方法定义在一个文件中的,可以提供给所有 计算属性 使用,它只定义了一个属性 lazy : true。我们查看 Watcher 的源码,第四个入参 options 不为空且 this.lazy 被标记为 true;接着省略大段代码,走到最后的求值,因为lazy为true,所以这个计算属性 this.person1 现在的值是 undefined
this.value = this.lazy ? undefined : this.get()
4:求值完毕,再次回到 initComputed 方法中,接着一个简单的判断 key 是否重复
5:defineComputed(代码贴在了最下方) 它是干嘛的?简单来说,就是给通过Object.defineProperty 给 vm 添加新字段,并且修改该字段的 get 函数,这个 get 函数正是在 defineComputed 中创建的。进入 defineComputed 方法,在这里 if 逻辑能够直接进入,接着就是通过 createComputedGetter(代码也在最下方) 创建一个 get 函数,并且赋值给 sharedPropertyDefinition.get
6:sharedPropertyDefinition 是什么?它是一个共享对象,初始化计算属性时共享的一个对象,结构如下,就是用来提供给 Object.defineProperty 使用的
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
7:最后 Object.defineProperty 执行完毕之后,vm上就多一个叫 person1 的字段,且现在的值还是为 undefined
(跳过下面一段源码继续看文章)
const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { 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. watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. 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) } } } }
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) }
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 } } }
计算属性的值从哪儿来?
看到这里,initComputed 的逻辑已经走完了,但是 vm.person1 的值还是undefined,如果我们在代码中的任何一个地方都不去获取这个值,那么它永远都是undefined,当我们在代码 或者 在模板 中获取这个值的时候,它才会进行第一次求值的操作。
接下来是重点,请仔细查看源码
1:首先,我们在 created 中打印这个值 ,它会触发 get 逻辑,也就是 createComputedGetter 这个方法创建的 get 函数,注意,computedGetter 这个函数的是通过 vm 触发的,所以它的this也是指向的 vm,通过key获取到对应的 computed-watcher,正常情况下这个watcher是可以获取到的(服务端渲染的情况下是没有的)
created(){ console.log(this.person1); }
2:watcher.dirty,这个字段的值为 true,是在 Watcher 的构造函数中赋值的,进入 evaluate 方法执行 get 求值,接着就是我们熟悉的操作,压栈-依赖收集-出栈-清理依赖,发生在render-watcher身上的事情,也发生在了 computed-watcher上
3:我们直接看到 Watcher 的 get 方法,还记得这个 getter 从哪儿来的吗?它在构造函数中赋值,这个 getter 正是我们自己写的计算函数,就如下面示例代码所写的那样
computed:{ person1(){ return 'my name is ' + this.person; } }
4:执行 getter ,就触发 this.person 的get 逻辑,它的逻辑我们在上篇文章的 依赖收集 讲的很清楚了,此时 Dep.target 正是刚刚才压栈的 computed-watcher,然后它们就可以建立依赖关系了。
5:顺利求出了 value 的值,又返回 evaluate 方法,watcher.value 被赋值了,它就是 person1 现在最新的值,console.log(this.person1) 也就能打印出这个值了。dirty在此时被改为了 false,回头看看 computedGetter 中的那段 watcher.dirty 的判断,由此我们可以知道 person1 在第一次求值结束之后 value会被缓存下来,后面再多次获取 person1 的时候始终都会取到缓存的值,只要 person1 依赖的 person 没有发生变化,person1 便不会发生任何变化
6:还是看到 computedGetter 方法,有一段 Dep.target 的判断,由上面求值的逻辑可以知道,computed-watcher已经出栈了,此时的 Dep.target 是谁的watcher呢?如果,获取 person1 的值仅仅发生在代码中,如下列代码所示,那么此时Dep.target为undefined,至于这里为什么是 undefined,打开源码去看看callHook这个方法就知道了。
created(){ console.log(this.person1) }
如果,此时在模板中 使用了这个计算属性({{ person1 }}),在 vm.render的过程中也会触发get ,并且此时的Dep.target正是 render-watcher,然后就能进入 watcher.depend,打开 Watcher 的源码查看 depend 方法,遍历 deps,我们知道这个deps保存的正是 computed-watcher搜集的依赖,挨个儿执行depend 方法,然后dep对象就能和当前处于栈顶的 render-watcher 建立依赖,这也就是为什么当我们在代码中改变 person 的值的时候,依赖 person1 的视图也会触发变化。
当 person 的值改变时,computed-watcher是可以接收到通知的,并且执行 update 方法,此时由于 computed-watcher 的 lazy 为 true,就会走第一段 if逻辑,this.dirty = true,我们在上面的 computedGetter 已经看到过 dirty 这个东西了,它被设置为 true,就表示 computed-watcher 取消 value的缓存了,所以当 render-watcher 执行 vm.render 的时候,依赖会重新收集,person1 的 value 也会被重新计算,视图也会展示出最新的值。
总结
至此,computed的原理已经讲完了,下篇我们接着讲 watch 的原理。
发表回复