「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。
前言
Computed 函数会根据传入的getter中依赖的响应式对象,返回一个新的响应式对象,当依赖的对象发生变化,新的对象也会跟着变
在之前的文章 Vue3.0源码学习——初始化流程分析(3.patch过程) 和 Vue3.0源码学习——更新流程分析 中学习了Vue3在初始化和更新的过程。在渲染函数 baseCreateRenderer 中有个副作用安装函数 setupRenderEffect,其中一个对象 effect 是从 ReactiveEffect 这个类创建的实例,会将组件更新函数 componentUpdateFn 作为参数传入,最终会在依赖发生变化时进行对应更新操作
本文将结合 ReactiveEffect 类和 Computed 函数源码进行学习
源码分析
- 找到
computed的位置\packages\reactivity\src\computed.ts,首先可以看到源码中对computed做了几次重载,因为在使用computed时第一个参数可以是 getter 函数,也可以是一个具有get和set函数的对象
- computed 最终返回的
cRef是一个 ref 对象, 是通过ComputedRefImpl创建的实例,看名字就和之前学过的创建 ref 对象的构造函数RefImpl很像,Vue3.0源码学习——响应式原理(三)
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 如果是函数,传入getter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
...
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建computed的ref
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
...
return cRef as any
}
- 来到
ComputedRefImpl中,constructor中定义的this.effect就是用响应式副作用函数ReactiveEffect创建的实例,当 computed 的依赖发生变化就触发triggerRefValue然后重新执行getter
class ComputedRefImpl<T> {
...
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// 创建响应式副作用,ReactiveEffect第二个参数scheduler会触发第一个参数fn执行
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.active = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
- 第一次当组件渲染会触发 computed 的
get value(),返回self.effect.run()的执行结果,其实就是返回的getter函数的执行结果,因此在使用时可以通过computed.value获取值
get value() {
// 可能被其他代理包装过,因此先拿到原始数据
const self = toRaw(this)
// 首次执行,收集依赖
trackRefValue(self)
// 立刻执行一次副作用
if (self._dirty) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
ReactiveEffect 类片段,第一个参数 fn 就是传入的 getter
调试
准备工作
首先做一个简单的例子,定义一个响应式的数据 state.counter,一个定时器每秒state.counter都会 +1,一个 double 是 computed 函数的返回值,等于 state.counter * 2
<body>
<div id="app">
<h1>computed</h1>
<p>{{ state.counter }}</p>
<p>{{ double }}</p>
</div>
<script>
const app = Vue.createApp({
setup(props, { emit, slots, attrs }) {
const state = Vue.reactive({
counter: 1
})
setInterval(() => {
state.counter++
}, 1000)
const double = Vue.computed(() => state.counter * 2)
return {
state,
double
}
}
})
app.mount('#app')
</script>
</body>
-
在
computed函数中打断点,刷新页面,第一次执行computed函数,然后找到返回值cRef并打上断点,然后单步进入 -
进入
ComputedRefImpl类,在get value()中打断点,然后继续执行 -
此时看一下堆栈信息,依次执行了这些函数,然后触发了
computed的get value()- componentUpdateFn 组件更新函数
- renderComponentsRoot 根组件渲染函数
- render 页面渲染函数
-
可以看到就是页面上使用的
double这个 computed 数据 -
回到
get value()中,会继续调用trackRefValue,单步进入,此时还没有收集到依赖dep,因此会创建一个, 然后就会执行到trackEffects去收集依赖 -
可以看到收集到了有2个依赖
- 组件中使用的
double getter中依赖的对象state.counter
- 组件中使用的
-
这形成了一个依赖响应的“链”,依赖发生了变化,其他都会响应的变化
state.counter->double->组件
- 在
new ReactiveEffect传入的第二个参数scheduler中打上断点,看一下更新的过程,然后将代码放过去,看到页面已经渲染出来了
-
查看调用栈可以看到是之前例子中设置的
setInterval激活了computed的更新操作 -
继续往下走进入
triggerRefValue,会执行triggerEffects -
triggerEffects会循环依赖去做更新的操作