前言
写过vue的同学,写过computed的都知道,computed会在依赖属性发生变化的时候自动更新结果。
他有一个重要的特点:计算值是可缓存的,只有依赖项发生变化的时候,才会重新计算
而通过之前的文章,我们已经了解了reactive,ref的实现原理,相信大家已经对vue3响应式机制有所了解,今天我们就来了解一下computed是如何实现的。
注:computed的源码难度相当大,我会尽力描述清楚其实现原理,如有不足之处,还请见谅
案例
1 | const obj = reactive({ |
以上代码运行后,我们可以看到如下现象
- 页面显示:我叫张三
- 2s后,页面显示我叫李四
按照我们之前源码分析的思路,我们依旧从以下三个角度入口
- 初始化
- 读取(依赖收集)
- 赋值(依赖触发)
接下来就让我们走进computed的源码世界吧~
computed初始化
1 | // 入口函数 |
入口函数的逻辑还是非常简单的,如果传入的是一个匿名函数,这处理为getter,如果传入的是对象,这赋值getter setter,这部分逻辑符合我们对这个API的使用习惯,也解释了computed为何是这样的传参方式。
抹平两种传参方式的差异后,new ComputedRefImpl,并返回,所以computed = new ComputedRefImpl ,我们接下来就进入该Class中看看吧。
1 | // 计算属性的响应式也是通过class get set去实现的 |
在ComputedRefImpl初始化阶段,我们看到了非常熟悉的api,ReactiveEffect,在我们的前面的reactive,ref源码分析中,我们使用这个api来完成关键步骤依赖收集,不过这里有些区别,传入了第二个参数,一个匿名函数,目前还无法体现其作用,我们后面再说
总的来说,ComputedRefImpl初始化阶段,生成了一个ReactiveEffect并保存到当前类的effect变量中。
依赖收集
按照我们实例代码,首次访问effect初次执行的时候,我们会触发showName.value
的get,也就是说,会触发ComputedRefImpl的get。
1 | // 被读取的时候触发 |
当我们触发computed的get的时候,首先会触发trackRefValue,将当前activeEffect收集到ComputedRefImpl的dep中,这正是依赖收集,这里effect被收集到了computed的dep中,建立起了computed与其被依赖项(effect)的联系。
然后判断**_dirty是否为true,默认是true,所以进入判断中,首先将_dirty改为false,下一次则不会进入判断,直接返回computed**之前的结果,之后再执行computed初始化阶段声明的ReactiveEffect,也就是我们computed本身的effect。
computed的effect.run一旦触发,全局activeEffect将会被替换为当前computed的 effect的fn ,并且触发computed依赖项obj.name的get,进而触发proxy的依赖收集,于是obj.name成功收集到了computed内部的effect,proxy与computed建立了联系,同时返回了最新的computed结果。
computed的get行为触发的时候,我们发现computed收集了effect,reactive收集了computed,三者之间建立起了联系。
关于_dirty
现在我要个大家着重讲一下ComputedRefImpl中的这个参数,_dirty是实现计算属性缓存性的关键所在,
我们假设一下,没有缓存性的computed,是什么样的运行逻辑
计算属性依赖了变量abc,并返回abc的总和,每个获取计算属性的时候,我都需要计算一次abc的总和,即使abc这三个值没有发生任何变化,就是这样的
依赖触发
2s后,我们触发了obj.name的set,所以首先触发obj.name的依赖触发,此时我们将可以通过WeakMap会找到之前收集到computed,我们直接进入依赖触发的逻辑。
1 | export function trigger( |
计算属性的触发逻辑还是非常复杂的,首先proxy的set,触发computed的scheduler(调度器),scheduler通过computed的dep找到相关effect,effect的fn执行又会触发computed的get,并与首次完成computed的计算,同时缓存最新的computed的结果,进而再完成effect的全部逻辑。
代码执行流程
依赖收集阶段
- computed初始化阶段,通过ReactiveEffect进行初始化,并且生成scheduler(调度器)
- effect初始化,触发computed的get,将当前activeEffect(effect)收集到computed的dep中(computed将effect收集)
- 执行computed自身逻辑,刷新全局activeEffect
- 进而触发proxy的get事件触发,将当前activeEffect(computed)收集到WeakMap中(proxy将computed收集)
- proxy的返回值返回computed,完成computed的计算逻辑
- 获取到computed结果,完成effect
依赖触发阶段
- 触发proxy的set,set行为中触发依赖,触发之前保存的computed的调度器scheduler(proxy找到computed)
- 调度器scheduler触发,dirty改为true,同时触发computed中保存的依赖,其中都是相关effec的fn。(computed找到effect)
- effect触发,fn执行,触发computed的get行为
- dirty为true,首次进行计算属性的重新计算(除非依赖项改变,否则下次不会重新计算),返回最新的computed结果,
- effect执行完成
回答一些问题
computed如何实现高性能缓存的?
通过调度器scheduler + 脏值检查_dirty,实现依赖项不变化,不进行重新计算,依赖项变化后仅执行一次的逻辑,进而实现高性能缓存。
为什么访问computed需要.value
因为我们访问computed实际上是访问ComputedRefImpl这个Class的实例,他的内部通过get value返回被访问值,所以我们必须通过**.value**来访问
简述computed的实现原理?
vue的响应式api都可以从依赖收集 依赖触发2个角度出发阐述其原理实现
依赖收集阶段:computed通过首次get的完成相关effect的依赖收集,首次计算的时候proxy完成computed的依赖收集。
依赖触发阶段:computed的依赖项发生变化后,会通过proxy找到computed的调度器 scheduler,触发所有effect,effct中再出发computed的get,首次get将进行一次结果运算(后续不在运算,除非computed依赖项发生变化),effect触发完成
总结
到此为止,我们computed的核心源码就解读完毕了,虽然总体依旧可以从依赖收集和依赖触发两个角度去理解实现原理,但是新增加的scheduler(调度器)与**_dirty(脏值检查)**机制,让逻辑复杂了很多。
大家在理解computed源码的时候,一定要多走几遍流程,多捋几遍逻辑。