「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
在上一篇小作文中,我们详述了 new Vue 实现数据响应式的方法 initState 方法中继 initProps 方法后的 initMethods、initData 和 initComputed;
但是由于篇幅的问题,initComputed 有一部分细节移到本篇中,所以本篇的重点还是 initComputed。我们回忆一下 initComputed 方法都做了什么:
- 创建
vm._computedWatchers对象,这个对象用于保存用computed创建的watcher,每个计算属性都有 - 遍历
computed,即vm.$options.computed,给每个key都用new Watcher创建一个watcher,并且保存早vm._computedWatchers上 - 调用
defineComputed(vm, key, userDef)方法将每个key代理到vm上,每个key就是一个计算属性 - 判重处理,与
props、methods、data中的key对比,不能出现重复的key
二、defineComputed 方法
方法位置:src/core/instance/state.js -> defineComputed
方法参数:
target对象,接收到的是vmkey属性,接收到的是computed中的keyuseDef,用户定义computed key对应的定义对象或者函数,当然在这里收到一个函数
方法作用:将 computed 上的 key 访问代理到 vm 上;
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() // 非 ssr,shouledCache 为 true
// 根据 userDef 参数构造 computed 的配置对象,包含 get 和 set
// computed 的定义有两种方式:someComputed () { return this.xx + this.aa }
// 或者 someComputed: { get () { return this.xx + this.zz} }
// 当 userDef 为函数时就是第一种,else 就是第二种
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
}
// ....省去一些不重要代码
// 拦截 target.key 的访问和设置
Object.defineProperty(target, key, sharedPropertyDefinition)
}
三、createComputedGetter 方法
3.1 方法基础
方法位置:src/core/instance/state.js -> function createComputedGetter
方法参数:computed 中的 key
方法作用:根据给定 key 返回一个作为该计算属性的 getter 函数;在这个函数中主要做了以下事项:
- 用接收到的计算属性的
key从this._computedWatchers中找到其对应的watcher - 如果取到并且
watcher.dirty为true,注意这个watcher.dirty,则调用watcher.evaluate()方法求值。 - 如果
Dep.target有就调用watcher.depend();- 啥意思呢?表示
Dep.target所代表的watcher要收收集当前这个computed watcher所有的依赖。听起来很拗口,我来解释一下: 比如有几个计算属性,computed: { a(){ this.b + this.c }, b,c },就是说a是b、c两个计算属性之和,此时a被渲染到模板上。在渲染的时候Dep.target就是渲染 watcher,而a对应的计算属性watcher依赖了b和c,此时,Dep.target代表的渲染watcher就要收录b、c,以实现b/c变化时能重新渲染
- 啥意思呢?表示
- 返回
watcher.value属性,value是第二步evaluate的结果;
function createComputedGetter (key) {
return function computedGetter () {
// 得到当前 key 对应的 watcher
// this._computedWatchers 是在上面 initComputed 的时候添加到 vm 实例上的,vm 即 this
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
// 调用 watcher.evaluate 就是求值,求 watcher 接收到的 expOrFn 的值
return watcher.value
}
}
}
3.2 computed 的缓存问题
相信很多人都听过 watcher 和 computed 的区别之一就是 computed 是有缓存的,可能曾经你觉得它高深莫测,今天你就发现它就是一个标识符;
举个例子吧,我们 test.html 中定义了一个 someComputed 计算属性,假如我们在模板是这样的:
<div> {{ someComputed }} </div>
<div> {{{ someComputed }} </div>
接下来我们按流程分析,当 渲染 watcher 求值的时候,就会读取到 vm.someComputed,而 vm.someComputed 已经被 Object.defineProperty 所拦截,读取时将会调用前面其描述对象的 getter,而这个 getter 就是上面 3.1 createComputedGetter() 方法返回的函数,如下:
function createComputedGetter (key) {
return function computedGetter () {
// 这个就是 上面文字说的 getter 函数
}
}
}
这个 getter 执行就会执行 watcher.evaluate() 方法求值,但你会发现 computedGetter 函数中有一个判断: if(watcher.dirty) { watcher.evaluate() };
第一次执行的时候没有问题,watcher.dirty = options.lazy === true 的,但是 watcher.evaluate 的逻辑就会吧 watcher.dirty 赋值为 false。
关键就来了,上面一共会访问两次 someComputed 这个计算属性,第一次 watcher.dirty 为 true 执行 watcher.evaluate() 求值,但是第二次访问 someComputed 时,此时 watcher.dirty 变为了 false 就不会再调用 watcher.evaluate() ,而是直接返回了 watcher.value,此时 watcher.value 还是上一个被访问时求得的值,这所谓 computed 缓存;
既然有缓存,那么什么时候才能触发重新求值呢?当然是 watcher.dirty 变为 true 的时候,当 watcher.update 方法被调用的时候就会把 watcher.ditry 重新置为 true
四、Wather 原型方法和 computed
4.1 watcher.prototype.evaluate()
该方法仅用于对 watcher.lazy 为 true 的 watcher 进行求值。
所谓求值就是求创建 Watcher 实例传入的 expOrFn 的值。在 initComputed 时,我们传给 Watcher 的 expOrFn 是 vm.$options.computed[key] 代表的求解计算属性的 getter 函数,因为 lazy,所以在创建 Watcher 后得到 watcher.value 是个 undefined`。
export default class Watcher {
// ... public properties
constructor () {
}
evaluate () {
this.value = this.get()
this.dirty = false
}
}
4.2 Watcher.prototype.get
维护依赖收集时的 Dep.target 属性,然后调用通过解析 expOrFn 得来的 this.getter 方法,this.getter 的 this 绑定为 vm。
在 this.getter 执行过程中,伴随着很多的响应式数据的访问,就会触发前面 defineReactive 定义响应式数据的 getter,在这些 getter 中有一个 if(Dep.target) { dep.depend() },此时 Dep.target 的值就是当前 Watcher 实例 本身,dep 就会把这个 watcher 收录起来;
export default class Watcher {
// ... public properties
constructor () {
}
get () {
// 维护 Dep.target,Dep.target = this,this 是 Watcher 实例
pushTarget(this)
let value
const vm = this.vm
try {
// 执行回调函数,比如 updateComponent,进入 patch 阶段
value = this.getter.call(vm, vm)
} catch (e) {
// 错误处理
} finally {
// 当 deep 为 true 时,深度 watch
if (this.deep) {
traverse(value)
}
// 维护 Dep.target, Dep.target = null,
// 当前 watcher 求值结束,依赖收集也结束
popTarget()
this.cleanupDeps()
}
return value
}
}
4.3 Watcher.proptotype.update
当响应式数据发生改变时,即触发 setter 时,会由 dep.notify() 派发更新,dep 会找到自己收集的 watcher 们开工,调用各自的 update 用户触发更新。
在 initComputed 的过程中,update 的一个十分显著的作用就是重置 watcher.dirty 属性为 true,即 wather.lazy 为 true 的情况。这个操作使得计算属性再次被读取时调用 watcher.evaluate() 方法重新求值。这里算是和前的 3.2 comptued 的缓存问题 的一个呼应。
当有 watcher.sync 为 true,即需要同步更新,调用 watcher.run() 方法,这个不展开
最后一个作为压轴出演,即常规的队列更新,这个队列更新是个异步队列,其实从这里大家能感受到,watcher 和 computed 的另一个区别:computed 只能放同步逻辑,而 watcher 是可以处理异步逻辑的,说到底其实是两者的更新方式不同。
export default class Watcher {
// ... public properties
constructor () {
}
update () {
if (this.lazy) {
// 懒执行时走这里,比如 computed
// 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
this.dirty = true
} else if (this.sync) {
// 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
this.run()
} else {
// 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
queueWatcher(this)
}
}
}
五、总结
本文作为 initComputed 的收尾篇,主要讨论了以下问题:
- 详述了
defineComputed方法,它将crateComptuedGetter返回的 函数作为描述对象的getter,然后调用Object.defineProperty将计算属性代理到vm上; createComputedGetter方法,它返回一个函数,这个函数将是计算属性取值时的getter函数,这个函数获取计算属性对应的watcher,根据watcher.dirty是否调用watcher.evaluate()还是直接使用watcher.value这个缓存值;computed缓存的原理,已经重新求值的办法,核心在watcher.dirty这个属性- 最后讨论了
Wathcer.prototype上的evaluate/get/update方法的细节