「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」
前言
在Vue中,计算属性 computed 是我们经常用到的功能之一,它类似于watch,但是又有很大的区别。computed是Watcher类的最后也是最复杂的一种实例化的使。下面我们通过阅读源码,来看看它的实现原理。
Watcher
computed 是响应式的数据,所以computed在初始化和使用的时候都会用到Watcher类,我们简单说下Watcher。
Watcher的原理是先把自己设置到全局唯一的指定位置(windonw.target),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取真正读取数据的watcher,并把这个wathcer收集到Dep中去。通过这样的方式,watcher 可以主动去订阅任意一个数据的变化。
这里比较关键的就是在执行数据的getter时,会收集依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend() // 在getter中收集依赖
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
...
}
})
对于Watcher我们简单了解到这里就可以,不然掌开来讲的话篇幅就太大了。
computed 特点
computed 有两个特点:
- 在没有使用到 computed 的时候,不会运行 computed 的
getter - 在改变 computed 所依赖的响应式数据的时候,不会立即更新 computed 的值,一切都要等到使用 computed 的时候
computed 原理
我们从 computed 的这两个特点出发,分别对其进行分析
第一个特点
在Vue初始化的时候会在initState中调用initComputed方法对computed进行初始化。
// ./src/core/instance/state.js
function initState(vm) { // 初始化所有状态时
vm._watchers = [] // 当前实例watcher集合
const opts = vm.$options // 合并后的属性
... // 其他状态初始化
if(opts.computed) { // 如果有定义计算属性
initComputed(vm, opts.computed) // 进行初始化
}
...
}
我们来看下initComputed是怎么处理的
// ./src/core/instance/state.js
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null) // 创建一个纯净对象
for(const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{lazy: true}
) // 实例化computedWatcher
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
...
}
}
里面判断computed是函数类型还是对象类型,如果是函数则这个函数就是getter,否则使用computed.get。 获取到 getter 生成一个 Watcher,并设置lazy属性为true。这个lazy属性就是为了实现computed第一个特点。
// src/core/observer/watcher.js
class Watcher{
constructor(vm, expOrFn, cb, options) {
this.vm = vm
this._watchers.push(this)
if(options) {
this.lazy = !!options.lazy // 表示是computed
}
this.dirty = this.lazy // dirty为标记位,表示是否对computed计算
this.getter = expOrFn // computed的回调函数
this.value = this.lazy
? undefined
: this.get();
}
}
根据最后一句我们可以知道,如果lazy属性为true则不会直接调用get方法,所以才会在没有使用到 computed 的时候,不会运行 computed 的 getter。
第二个特点
在生成了 computed的Watcher之后,会继续执行defineComputed。
function defineComputed(target, key, userDef) {
...
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: createComputedGetter(key),
set: userDef.set || noop
})
}
执行 defineComputed方法,通过Object.defineProperty把computed变成响应式数据。
在createComputedGetter中,对getter进行封装
function createComputedGetter (key) {
return function () { // 返回getter函数
const watcher = this._computedWatchers && this._computedWatchers[key]
// 原来this还可以这样用,得到key对应的computed-watcher
if (watcher) {
if (watcher.dirty) { // 在实例化watcher时为true,表示需要计算
watcher.evaluate() // 进行计算属性的求值,调用传过去的 computed.get方法
}
if (Dep.target) { // 当前的watcher,这里是页面渲染触发的这个方法,所以为render-watcher
watcher.depend() // 收集当前watcher
}
return watcher.value // 返回求到的值或之前缓存的值
}
}
}
------------------------------------------------------------------------------------
class Watcher {
...
evaluate () {
this.value = this.get() // 计算属性求值
this.dirty = false // 表示计算属性已经计算,不需要再计算
}
depend () {
let i = this.deps.length // deps内是计算属性内能访问到的响应式数据的dep的数组集合
while (i--) {
this.deps[i].depend() // 让每个dep收集当前的render-watcher
}
}
}
在使用了computed数据后,会调用getter方法,computed 所对应的 Watcher 会被收集,所以才会在里面的响应式数据改变后,computed计算属性跟着改变。
到这里我们就把computed的原理大致讲完了,如果需要完全理解,可能需要对vue数据响应原理有个较深的认识,比如Dep、Watcher等。
computed 缓存机制
computed的计算属性还具有有缓存机制,只有当其依赖的响应式数据发生变化时才会清空缓存重新计算结果,其实上面的分析已经有了,就是通过 dirty属性,只有dirty为true时才会重新计算结果替换缓存。dirty只有当其响应式数据发送变化时才会设置为true,重新计算后会再次被设置为false。
if (watcher.dirty) { // 在实例化watcher时为true,表示需要计算
watcher.evaluate() // 进行计算属性的求值,调用传过去的 computed.get方法
}
总结
我们对上面这个流程做个总结。
- 在初始化Vue的时会执行
initComputed,在里面生成lazy属性为true的Watcher,Watcher根据lazy属性不立即执行get方法。 - 然后执行
defineComputed方法,通过Object.defineProperty把computed变成响应式数据。 - 在使用了
computed数据后,会调用getter方法,computed所对应的Watcher会被收集,所以才会在里面的响应式数据改变后,computed计算属性跟着改变。