「这是我参与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
会循环依赖去做更新的操作