计算属性 computed
计算属性computed会基于其响应式依赖被缓存,并且在依赖的响应式数据发生变化时重新计算。
计算属性也是一个ref对象。
测试用例
js
<div id="app"></div>
<script>
const { computed, reactive, effect } = Vue;
const obj = reactive({
name: "zh"
})
const computedObj = computed(() => {
return "执行" + obj.name
})
effect(() => {
// 触发ComputedRefImpl 的 get value
document.getElementById("app").innerHTML = computedObj.value
})
setTimeout(() => {
obj.name = "llm"
}, 2000)
</script>
我们先来介绍一下执行流程,再看断点调试。
- 上来先执行computed,创建一个computedRef对象。(所有computed对象都是Ref对象)
- 初始化computedRef对象时,创建一个
ReactiveEffect
对象。并将computed的getter函数传入。
- 然后执行effect,创建
ReactiveEffect
对象,并将effect
回调传入。 - 然后
computedObj.value
触发computed对象的get value, 收集执行effect创建的ReactiveEffect
对象。
- 通过
_dirty
变量控制computed的getter执行,触发reactive对象的getter方法,收集依赖(收集的是创建computedRef对象时内部创建的ReactiveEffect
对象)。(这里非常重要的一点,只要触发computed get value就有可能重新执行computed的getter) - 2s后,触发reactive对象的setter方法,触发依赖执行。这里就需要注意了。由于触发的是computedRef对象时内部创建的
ReactiveEffect
对象,上面挂载的有computed,并且有scheduler
调度器,所以会先执行含有computed属性的依赖具有scheduler
调度器的依赖。
- 执行调度器,调度器中触发computed对象get value收集的依赖。此时
document.getElementById("app").innerHTML = computedObj.value
执行,又触发computed get value, 执行computed 的getter 方法,返回修改的值。
断点调试
- 初始化computed
- 触发computed get value,进行依赖收集,并执行computed传入的getter方法
- 2s后触发reactive setter,然后触发依赖函数。此时该依赖有computed对象,所以调用
scheduler
调度器
- 触发effect回调,又会触发computed对象的get value。获取最新值。这里需要注意,虽然2s后触发了reactive的setter方法,但是并没有在trigger中直接执行computed的getter函数,而是通过再次触发computed get value通过
_dirty
变量来控制getter的触发的。
computed如何做缓存
当我们多次调用computedRef.value时,他会将computed reactiveEffect对象也加入到computedRef对象的deps中,在触发依赖时,如果不特殊处理,就会造成死循环,不会做缓存。
所以我们需要在触发依赖的时候先触发具有computed
属性的依赖,再触发普通响应式依赖。
总结
- 创建computedRefEmpl实例,内部通过_dirty变量判断是否触发依赖。触发依赖放在ReactiveEffect的调度器中执行,这样就可以区分普通的响应式数据和computed响应式数据执行了。并且先去触发computed的依赖函数,再去触发普通响应数据的依赖函数。(这样是为了做到computed缓存的)
- 获取computed变量时,触发get value执行,然后收集依赖。并执行传入的依赖getter。并修改
_dirty
为false,如果依赖数据未变化,那么它将返回缓存的值。
只要修改响应式数据,就会触发调度器执行,然后_dirty
设置为false,然后就会再次重新执行getter,拿到最新值。
watch
测试用例
js
const { watch, reactive, effect } = Vue;
const obj = reactive({
name: "zh"
})
watch(obj, (val) => {
console.log("val", val)
})
setTimeout(() => {
obj.name = "llm"
}, 2000)
- 调用watch,创建
ReactiveEffect
对象。 - watch依赖收集
- 执行getter,拿到值,赋值给oldValue保存
- 2s后触发reactive的setter方法,触发依赖,依赖是具有scheduler的,所以执行调度器,即job函数。
- 触发watchCallback
这里需要注意一下,监听对象的变化,我们获取新旧值是一样的,经过上面的分析我们就可以看出,因为oldValue是执行ReactiveEffect中的fn返回的,它返回的是一个对象类型。新值也是这个对象,所以setter修改时,引用不变,所以新旧值是一样的。
断点调试
- 调用doWatch函数执行初始化watch
- 进行判断,如果是reactive对象,那么就深度监听。
- 对象,递归调用,触发依赖收集
- 定义job函数
- 初始化ReactiveEffect对象和调度器。
任何关于响应式的api内部都离不开ReactiveEffect
类的初始化,他就是通过Proxy get拦截器收集ReactiveEffect
对象作为依赖,在触发Proxy set拦截器时,查看是否有scheduler
回调(computed 触发get value的回调,watch第二个参数),如果有就执行,没有就执行普通的响应式回调。
调度器Scheduler
- 控制代码执行顺序。
js
const { reactive, effect } = Vue
const obj = reactive({
age: 1
})
effect(() => {
console.log("=======", obj.age)
}, {
scheduler() {
setTimeout(() => {
console.log("=======", obj.age)
})
}
})
obj.age = 2
console.log("执行结束!")
由上图可知,执行结束先于=====,2
输出。
- 控制代码执行逻辑。
js
const { reactive, effect, queuePostFlushCb } = Vue
const obj = reactive({
age: 1
})
effect(() => {
console.log("=======", obj.age)
}, {
scheduler() {
queuePostFlushCb(() => {
console.log("=======", obj.age)
})
}
})
obj.age = 2
obj.age = 3
console.log("执行结束!")
由上图可知,跳过了obj.age = 2
的setter逻辑触发。
原理就是通过微任务队列执行调度器中的任务。
经过前面的分析,我们发现,scheduler对于计算属性和watch是非常重要的。
xdm,一起学习vue核心思想吧,为了能突破现状,加油啊。天天对着表单表格看,人都傻掉了。
往期文章
专栏文章
最近也在学习nestjs,有一起小伙伴的@我哦。