对于响应性系统而言,除了在前两篇文章接触的 reactive 和ref之外,还有常使用到的,那就是:计算属性:computed
。今天我们一起探究computed的响应性是如何实现的吧~
一、构建ComputedRefImpl
访问计算属性的值,必须通过.value ,因为它内部和 ref 一样是通过 get value 来进行实现的:
import { isFunction } from "packages/shared/src/index"
import { Dep } from "./dep"
import { ReactiveEffect } from "./effect"
import { trackRefValue } from "./ref"
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
constructor(getter) {
this.effect = new ReactiveEffect(getter)
this.effect.computed = this
}
get value() {
trackRefValue(this)
this._value = this.effect.run()
return this._value
}
}
export function computed (getterOrOptions) {
let getter
const onlyGetter = isFunction(getterOrOptions)
if(onlyGetter) {
getter = getterOrOptions
}
const cRef = new ComputedRefImpl(getter)
return cRef
}
每次 .value 时都会触发 trackRefValue 即:收集依赖
二、computed的响应性-调度器和脏状态的处理
以上代码完成后,我们在get value中做到了依赖收集,但是还未做依赖触发,所以还不是响应性的,接下来就要做的是触发依赖。
首先需要实现_dirty 脏变量,当_dirty为true时表示我们需要重新执行run方法。另外就是在ReactiveEffect中传入第二个参数,也就是scheduler调度器。当脏状态为假的时候,将脏状态置为真,并且触发依赖。
public _dirty = true
constructor(getter) {
this.effect = new ReactiveEffect(getter, () => {
if(!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
}
get value() {
trackRefValue(this)
if(this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
至此就完成依赖的触发了吗?当然不是!我们的scheduler还并未执行,所以我们需要在triggerEffect中做一些处理:
export function triggerEffect(effect: ReactiveEffect) {
if(effect.scheduler) {
effect.scheduler()
}else {
effect.run()
}
}
接下来测试一下我们的逻辑:
<body>
<div id="app"></div>
</body>
<script>
const { reactive, effect, computed } = Vue
const obj = reactive({
name: 'zhangsan',
age: 18
})
const computedObj = computed(() => {
console.log('computedObj')
return '姓名:' + obj.name
})
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
document.querySelector('#app').innerHTML = computedObj.value
})
setTimeout(() => {
obj.name = 'lisi'
}, 2000);
</script>
出现了死循环,仔细分析发现出现死循环的原因:执行第二个effects时,ReactiveEffect的computed被触发的时候,再次执行脏状态,再次执行triggerRefValue,会导致重新执行for循环,从而进入死循环:
三、computed的缓存性
基于以上调试和分析,我们在effects的循环中,先执行有computed的effect,即可避免上述死循环,所以我们改造如下:
export function triggerEffects(dep: Dep) {
const effects = Array.isArray(dep) ? dep : Array.from(dep)
// 依次触发依赖
for(const effect of effects) {
if(effect.computed) {
triggerEffect(effect)
}
}
for (const effect of effects) {
if(!effect.computed) {
triggerEffect(effect)
}
}
}
结语
以上代码和解析秉承了最少的代码来实现vue3源码,并未考虑到一些边缘状态等其他情况,以理解原理为主。
有些地方表述不够详细,若想彻底搞懂还需要自己写写代码并打断点执行下看看逻辑到底是怎么样执行的~
一起加油吧~