Vue3响应式对象-计算属性和异步计算属性

一、前言

在之前的博客中,已经针对ref对象和reactive对象做了详细的分析,这2种对象覆盖了常规类型以及对象类型的响应式设计。计算属性就是在这2种对象的基础上,实现的最后一种响应式对象。

计算属性分为2类,计算属性和异步计算属性。其中计算属性是最为常用的,异步计算属性在官方文档上没有说明和文档,其在计算属性的基础上更进一步进行了优化,有必要进行了解。

二、计算属性的意义

ref对象和reactive对象可以基本实现所有类型数据的响应式后,计算属性存在的目的在于优化。

当一些复杂的数据结构需要多个响应式对象来共同实现时,我们可以选择使用方法或者计算属性。如果使用方法,这些方法在render渲染函数重新执行时,都会被重新执行,即便这些方法没有使用到变更的依赖。如果在这种情况下使用计算属性,则会因为其内部的缓存设计不会重新计算,直接获取缓存值。

三、计算属性

1.核心代码解析

  • 创建计算属性```export function computed(getterOrOptions: ComputedGetter | WritableComputedOptions,debugOptions?: DebuggerOptions,isSSR = false) {let getter: ComputedGetterlet setter: ComputedSetter// 如果传入的是一个方法,则为只读const onlyGetter = isFunction(getterOrOptions)// 分别设置getter方法和setter方法if (onlyGetter) {getter = getterOrOptions// 不存在setter也会给一个settersetter = DEV? () => {console.warn(‘Write operation failed: computed value is readonly’)}: NOOP} else {getter = getterOrOptions.getsetter = getterOrOptions.set}// 创建计算属性,默认非服务端渲染const cRef = new ComputedRefImpl(getter, setter, onlyGetter || <img src=“https://juejin.cn/post/7165784179681001502 “https://juejin.cn/post/7165784179681001502”)简要了解,下一篇章详解。然后设计了一些配置相关的属性。* 读这是计算属性的重点,核心逻辑是判断当前缓存数据是否脏数据,是脏数据就重新计算。其中ReactiveEffect对象的run方法会调用getter方法进行计算。* 写直接调用setter方法,在创建计算属性时,如果未传入setter也会默认生成一个setter方法,详见创建代码。* 依赖变更时ReactiveEffect对象在创建时的入参中,第一个参数是getter方法,这个方法会进行依赖收集和数据计算。第二个方法则是依赖变更时的回调方法,这个方法将数据标识为脏数据,表示下次读取时需要重新计算,并且触发依赖更新。### 2. 响应式流程” style=“margin: auto” />

需要注意的是,计算属性的依赖数据变更时,并没有立刻重新计算新值,而是置为脏数据,待下次读取时重新计算,正因为这个特性,异步计算属性才进行了更一层优化

四、异步计算属性

1.计算属性的缺陷

虽然计算属性是常用属性,且并没有什么副作用,但它其实存在一个性能上的缺陷。在上面流程图中,响应式对象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次都是无效计算。当存在很多复杂逻辑时,这些无效计算无疑会极大的增加性能消耗。异步计算属性就是为了解决这种情况。

2.异步计算属性核心代码

// 异步计算属性的异步执行队列相关代码
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<T> {public dep?: Dep = undefinedprivate _value!: Tprivate _dirty = truepublic readonly effect: ReactiveEffect<T>public readonly __v_isRef = truepublic readonly [ReactiveFlags.IS_READONLY] = trueconstructor(getter: ComputedGetter<T>) {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个响应式对象。时刻图如下:

  • T0时刻在T0时刻,由Dep依赖回调读取deferComputed2为起点,构建了相关的依赖关系。* T1时刻在T1时刻,data数据变更,由此触发依赖更新。此时computedData置为脏数据,继续触发依赖更新。deferComputed1变更为脏数据,保存当前数据为比对数据,将数据比对操作放入异步队列,接着遍历其依赖列表,触发其中的异步计算属性的回调。deferComputed2置为脏数据,保存当前值作为比对值,由于是由异步计算属性触发,因此不再将比对操作放入异步队列,它的依赖列表中没有异步计算属性,因此不在继续触发依赖更新,因此calls等于1。* T2时刻执行数据比对,deferComputed1获取最新数据,由于是脏数据,需要重新计算,读取computedData,此时computedData是脏数据,也要计算最新值。deferComputed1发现前后数据变更,由此触发依赖更新,deferComputed2开始比对数据。deferComputed2由于是脏数据,也要重新计算,数据依旧变更,继续触发依赖更新,Dep依赖执行回调,calls等于2。上面着重分析了各个时刻的执行流程,其中还有个细节是很有意思的。异步队列是采用的微任务队列,当一个微任务在执行中又添加了另一个微任务,则这个后添加的微任务也会在本轮待执行的微任务中,而不是放到下一次执行微任务队列时执行。在T2时刻,deferComputed1触发的异步队列执行是执行的微任务,此时触发依赖更新会导致deferComputed2添加比对操作到异步队列中。这就符合一个微任务添加了另一个微任务,这2个微任务会同步执行,而不是异步执行。

五、总结

关于响应式对象的介绍到此结束,下面会继续分析响应式系统的核心ReactiveEffect对象。

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值