在之前的博客中,已经针对ref对象和reactive对象做了详细的分析,这2种对象覆盖了常规类型以及对象类型的响应式设计。计算属性就是在这2种对象的基础上,实现的最后一种响应式对象。
计算属性分为2类,计算属性和异步计算属性。其中计算属性是最为常用的,异步计算属性在官方文档上没有说明和文档,其在计算属性的基础上更进一步进行了优化,有必要进行了解。
在ref对象和reactive对象可以基本实现所有类型数据的响应式后,计算属性存在的目的在于优化。
当一些复杂的数据结构需要多个响应式对象来共同实现时,我们可以选择使用方法或者计算属性。如果使用方法,这些方法在render渲染函数重新执行时,都会被重新执行,即便这些方法没有使用到变更的依赖。如果在这种情况下使用计算属性,则会因为其内部的缓存设计不会重新计算,直接获取缓存值。
需要注意的是,计算属性的依赖数据变更时,并没有立刻重新计算新值,而是置为脏数据,待下次读取时重新计算,正因为这个特性,异步计算属性才进行了更一层优化。
虽然计算属性是常用属性,且并没有什么副作用,但它其实存在一个性能上的缺陷。在上面流程图中,响应式对象B每变更一次,就会执行一次相应的依赖更新流程,这往往是不合理的,因为在同步执行的情况下,只有最后一次变更会生效。示例如下:
let data = ref(1)
let computedData = computed(() => {return data.value+1
})
let calls = 0
let rec = 0
effect(() => {rec = computedData.valuecalls++
})
expect(calls).toBe(1)
data.value=2
data.value=3
data.value=4
expect(calls).toBe(4)
expect(rec).tobe(5)
可以看出,rec变量的值,取决于最终的那一次计算出的值。但整个计算却是进行4次,这表示有3次都是无效计算。当存在很多复杂逻辑时,这些无效计算无疑会极大的增加性能消耗。异步计算属性就是为了解决这种情况。
// 异步计算属性的异步执行队列相关代码
const tick = /*#__PURE__*/ Promise.resolve()
const queue: any[] = []
let queued = false
const scheduler = (fn: any) => {// 加入队列queue.push(fn)if (!queued) {queued = true// 同步任务执行完成后执行异步队列tick.then(flush)}
}
const flush = () => {// 执行异步队列里面的所有方法for (let i = 0; i < queue.length; i++) {queue[i]()}queue.length = 0queued = false
}
// 计算属性的实现
class DeferredComputedRefImpl {public dep?: Dep = undefinedprivate _value!: Tprivate _dirty = truepublic readonly effect: ReactiveEffectpublic readonly __v_isRef = truepublic readonly [ReactiveFlags.IS_READONLY] = trueconstructor(getter: ComputedGetter) {let compareTarget: anylet hasCompareTarget = falselet scheduled = false // 是否已经确定异步执行this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {// 如果存在依赖if (this.dep) {// 是否是由异步计算属性触发的更新if (computedTrigger) {// 当前数据设置为对比数据compareTarget = this._valuehasCompareTarget = true} else if (!scheduled) {// 获取对比数据const valueToCompare = hasCompareTarget ? compareTarget : this._valuescheduled = truehasCompareTarget = false// 放入异步队列等待执行scheduler(() => {// 获取最新数据与对比数据进行比较,变更了则触发更新if (this.effect.active && this._get() !== valueToCompare) {triggerRefValue(this)}scheduled = false})}for (const e of this.dep) {// 如果依赖中存在异步计算属性,则主动触发回调执行if (e.computed instanceof DeferredComputedRefImpl) {e.scheduler!(true /* computedTrigger */)}}}// 置为脏数据this._dirty = true})this.effect.computed = this as any}private _get() {// 脏数据重新计算if (this._dirty) {this._dirty = falsereturn (this._value = this.effect.run()!)}return this._value}get value() {// 收集依赖trackRefValue(this)// the computed ref may get wrapped by other proxies e.g. readonly() #3376return toRaw(this)._get()}
}
核心注释已经标注。和计算属性相比,主要的差异点如下:
我们主要分析下这个回调方法。这个方法的意图只有1个,保存当前的数据作为比对数据A,然后异步执行数据比较,如果比对数据A和最新数据不一致,则触发依赖更新。
其中存在的判断逻辑和主动触发异步计算属性的回调方法是为了处理异步计算属性的依赖关系。相关代码如下:
if (computedTrigger) {// 当前数据设置为对比数据compareTarget = this._valuehasCompareTarget = true
}
for (const e of this.dep) {// 如果依赖中存在异步计算属性,则主动触发回调执行if (e.computed instanceof DeferredComputedRefImpl) {e.scheduler!(true /* computedTrigger */)}
}
下面使用如下示例代码结合图解分析:
let data = ref(1)
let computedData = computed(() => {return data.value + 1
})
let deferComputed1 = deferredComputed(() => {return computedData.value + 1
})
let deferComputed2 = deferredComputed(() => {return deferComputed1.value + 1
})
let calls = 0
effect(() => {deferComputed2.valuecalls++
})
// T0时刻
expect(calls).toBe(1)
data.value++
// T1时刻
expect(calls).toBe(1)
await Promise.resolve()
// T2时刻
expect(calls).toBe(2)
示例代码中,一共有4个响应式对象。时刻图如下:
关于响应式对象的介绍到此结束,下面会继续分析响应式系统的核心ReactiveEffect对象。
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享