共计 3856 个字符,预计需要花费 10 分钟才能阅读完成。
写在前面:本文为个人在日常工作和学习中的一些总结,便于后来查漏补缺,非权威性资料,请带着自己的思考 ^-^。
前文链接:了解一下 Vue – [Vue 是怎么实现响应式的(二)]
前言:上一篇文章简单介绍了基于 data 的 Vue 响应式的实现,这篇将进行一点扩展,data 变更会自动触发 computed 进行重新计算,从而反映到视图层面,那这个过程又是怎么做到的呢?
从 Vue 实例生成过程的 initComputed 说起
顾名思义,在 initComputed 中主要进行的工作是对 computed 进行初始化,上代码:
function initComputed (vm, computed) {const watchers = vm._computedWatchers = Object.create(null) // 用于存放 computed 相关的 watcher
for (const key in computed) { // 遍历 computed,为每一个 computed 属性创建 watcher 实例,这个 watcher 实例的作用后面会体现
// 这里可以看出,平时我们的 computed 一般都是函数形式的,但很多时候我们也可以写成{get() {}, set() {}},这种对象形式
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop, // 此函数参数会在 watcher 实例的 get 方法中进行执行
noop,
computedWatcherOptions // {dirty: true},可以翻一下之前的 class Watcher 代码或者找源码看一下,这个 options 其中的一个作用就在于控制实例化 watcher 的时候是否先执行一次 get() 方法,这个 get 方法内部会对参数传进来的 getter 进行执行)
if (!(key in vm)) {defineComputed(vm, key, userDef)
}
}
}
// function defineComputed
function defineComputed ( // 整体来说此函数的作用就是通过 defineProperty 定义 getter/setter 将 computed 中的属性代理到 vm 上
target: any,
key: string,
userDef: Object | Function
) {const shouldCache = !isServerRendering() // 非服务端渲染,则为 true
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key) // computed 的计算结果会被缓存,不需要每次访问 computed 属性时都重新进行计算
: userDef // computed 不使用缓存的情况
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
实例化 Vue 期间,对 computed 的处理,做了:
- 将 computed[key]的值作为 getter 参数,实例化一个 Watcher 对象(至于 Watcher 对象的作用,后面会提到);
- 将 computed[key]的值(其实是经过包装的)作为 getter,通过 defineProperty 将 computed[key]代理到 vm[key];
说到 $mount 的执行过程
$mount 包含了模板编译、render 函数生成 … 不再赘述
和响应式相关的是在 $mount 中实例化了一个 render Watcher,前文已经有过标注,在实例化 Watcher 中会执行 get()函数,从而执行 render 函数,在 render 函数的执行过程中势必会读取页面渲染使用到的 computed 属性,触发其 getter:
// computed 属性的 getter
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key] // watcher 就是 initComputed 时传入 computed[key]作为 getter 参数实例化的 watcher
if (watcher) {if (watcher.dirty) {// 如果是首次读取 computed[key],一般 watcher.dirty 为 true
watcher.evaluate()
/**
evaluate () {this.value = this.get()
this.dirty = false // 执行完 get 之后就会将 dirty 置为 false,这样下次读取 computed[key]时就不会再重新计算
}
执行 watcher.get(),在这个函数内部会做几个事情:执行 pushTarget 函数,将当前的 Dep.target 置为当前 watcher 实例
执行 computed[key],计算得到结果赋值给 watcher.value
如果 computed[key]函数内容是通过几个 data 计算得到值,则将会触发 data 的 getter,这将会把这个几个 data 的 dep 对象作为依赖添加至 watcher 的 deps 列表,同时将当前 watcher 添加至这些 dep 的 subs 列表中,通俗一点说,这个过程对于当前 watcher 来说就是依赖收集过程,将其依赖的项添加至 deps 中
对于某一个 data 的 dep 来说,就是将当前 watcher 添加至其观察者列表 subs 中
执行完以上过程,就会将 Dep.target 重置为之前的值,*/
}
if (Dep.target) {watcher.depend() // 这一步也很重要,此处是将当前 watcher 的依赖也加入到 Dep.target 的依赖列表中
/**
为什么要有这一步呢?因为当前的 Dep.target 在执行完 watcher.evaluate 之后就被重置回了上一个 Dep.target,一般来说当前的 Dep.target
就是 render Watcher
设想有这种情况:某一个 data 的属性 x 并没有直接用于 render,那么在 render 执行过程的依赖收集 x 就不会被添加
到 render Watcher 的 deps 中,x 的 dep.subs 中也没有 render Watcher 也就是说之后如果对 x 进行重新赋值,则不会
通知 render Watcher,此时还没有问题,但时如果 x 被 computed 用到,由于 computed 没有 setter,则 x 被重新赋值
通知到 computed Watcher 去重新计算,但是 computed 并没有直接通知 render Watcher 的方法,这个时候 render 就不会
重新执行,页面也就不会进行更新。。。*/
}
return watcher.value
}
}
}
总之,详见代码注释。。。
当 computed 依赖的 data 更新时
这里已经知道对 data 重新赋值,会触发其对应 setter
// setter
set: function reactiveSetter (newVal) {if (newVal === value || (newVal !== newVal && value !== value)) {return}
val = newVal
childOb = !shallow && observe(newVal)
dep.notify() // 这里会通知到所有 watcher,让它们进行 update}
// dep.notify
notify () { // 在 render 阶段进行依赖收集时会将 watcher 加入 subs 列表,computed 在进行计算的时候会收集依赖的 data,// 与此同时会将 computed 的 watcher 对象添加至 data 的 dep.subs 列表
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
}
render watcher 和 computed watcher 执行 update 是不同的,computed watcher 的 update 方法只会将 watcher.dirty 置为 true,这代表该 computed 依赖的 data 发生了更新,需要重新计算;这样在 render 函数再次执行的时候会读取 computed,触发 computed 的 getter,在 getter 中会重新计算得出 computed 的新值,并且将 dirty 置为 false,代表只需计算一次,在同一个 render loop 中多次引用该 computed 将不会重新计算。
THE END…