本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 4 篇,关注专栏
前言
Vue3 中响应式系统除了 reactive
和 ref
这两个函数外,我们还需了解下 computed
和 watch
这两个函数,它们也是响应式系统的关键所在。我们知道 computed
计算属性是存在依赖关系,当依赖的值发生变化时计算属性也随之变化,接下来我们先看下 computed
是如何实现的。
案例
首先引入 reactive
、effect
、computed
三个函数,之后声明 obj
响应式数据和 computedObj
计算属性,接着又执行 effect
函数,该函数传入了一个匿名函数进行 computedObj
的赋值,最后两秒后又修改 obj
的 name
值。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, effect, computed } = Vue
// 创建响应式数据
const obj = reactive({
name: 'jc'
})
// 计算属性 触发 obj.name 的 get 行为
const computedObj = computed(() => {
return '姓名:' + obj.name
})
// effect 函数中 触发 计算属性的 get 行为
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
})
// 修改响应式数据的 name 值 触发 set 行为
setTimeout(() => {
obj.name = 'cc'
}, 2000)
</script>
</body>
</html>
computed 实现
computed
函数定义在 packages/reactivity/src/computed.ts
文件下:
ts
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// getterOrOptions 即传入的函数
// () => { return '姓名:' + obj.name }
const onlyGetter = isFunction(getterOrOptions) // 判断 getterOrOptions 是否为函数
if (onlyGetter) {
getter = getterOrOptions // 赋值 传入的函数
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly') // 理解为空函数
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建 ComputedRefImpl 实例
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
// 返回 实例
return cRef as any
}
这段逻辑也容易理解,computed
函数接收一个 getterOrOptions
参数,即我们传入的匿名函数 () => { return '姓名:' + obj.name }
:
之后赋值给 getter
,setter
我们可以理解为一个空函数,之后创建一个 ComputedRefImpl
实例,并将其返回,我们再看下 ComputedRefImpl
构造函数:
ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
public _dirty = true // 脏变量 关键
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self) // 依赖收集
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
该构造函数会创建一个 ReactiveEffect
实例,这块逻辑我们前面文章也已经讲过,这里就不再具体描述,我们先看下返回的实例 effect
:
另外我们还需关心 ReactiveEffect
传入的第二个参数 scheduler
,该构造函数在 packages/reactivity/src/effect.ts
文件下:
ts
// 传入的参数
() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
}
// ReactiveEffect 构造函数
export class ReactiveEffect<T = any> {
// 省略
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
// 省略
}
scheduler
我们可以理解为一个调度器,这也是 computed
核心所在,该逻辑会进行依赖触发,我们稍后再来讲解。ComputedRefImpl
还定义了一个 _dirty
脏变量,该变量用法也之后来讲解。另外还定义了 get value
和 set value
两个方法,这也是和 ref
相同,赋值时需带上 .value
属性的原因。get value
会进行依赖收集,但是依赖触发并没有在 set value
中,而是在我们之前 ReactiveEffect
传入的第二个参数中。
此时 computed
函数执行完毕返回 ComputedRefImpl
实例对象:
之后执行 effect
函数,进行赋值 document.querySelector('#app').innerHTML = computedObj.value
,从而触发 computed
的 get value
方法:
ts
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self) // 依赖收集
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
trackRefValue(self)
进行依赖收集,该方法在前面文章也讲到过。由于此时 _dirty
脏变量为 true
(ComputedRefImpl
构造函数默认为 true
),所以之后设置为 false
,再执行 self.effect.run()
进行赋值。我们知道 effect.run()
实际执行的是 fn()
方法,即 computed
传入的匿名函数 () => { return '姓名:' + obj.name }
,effect
函数执行完毕,页面呈现如下:
两秒后触发 obj
的 setter
行为,即执行 createSetter
方法进行 trigger
依赖触发(第一次),然后根据 name
属性获取到对应的 effects
,该逻辑都在 packages/reactivity/src/effect.ts
文件下:
之后 triggerEffects
函数遍历 effects
,执行 triggerEffect(effect, debuggerEventExtraInfo)
:
ts
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
// 处理死循环 先执行 computed 属性的 effect 再执行不含有 computed 属性的
for (const effect of effects) {
// 存在 computed 属性
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
这里我们需要关注下 if (effect.scheduler)
判断逻辑,由于此时执行的 effect
含有 computed
属性,且存在 scheduler
,则会执行 effect.scheduler()
方法:
这就是之前我们提到的 ComputedRefImpl
构造函数中,创建 ReactiveEffect
实例时传入的第二个参数:
ts
export class ComputedRefImpl<T> {
// 省略
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
})
// 省略
}
// 省略
}
// 第二个参数
() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
}
因为在 computed
函数的 get value
方法中 _dirty
设置了 false
,所以直接走判断逻辑,执行 triggerRefValue(this)
依赖触发(第二次),所以 computed
的依赖触发是在该逻辑中执行的,这里是关键。
我们再看下此时获取到的 effects
:
所以根据判断逻辑直接走 effect.run()
,我们知道执行 run
等于执行 fn
方法,即执行 effect
传入的匿名函数,之后执行 document.querySelector('#app').innerHTML = computedObj.value
赋值操作,再次触发 computed
的 get value
方法:
ts
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self) // 依赖收集
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
接着执行 self._value = self.effect.run()!
,又再次执行 computed
传入的匿名函数 () => { return '姓名:' + obj.name }
重新赋值:
代码执行完成,此时页面呈现修改后的值:
总结
computed
计算属性实际是一个ComputedRefImpl
构造函数的实例。ComputedRefImpl
构造函数中通过dirty
变量来控制effect
中run
方法的执行和triggerRefValue
的触发。- 想要访问计算属性的值,必须通过
.value
,因为它内部和ref
一样是通过get value
来进行实现的。 - 每次
.value
时都会执行get value
方法,从而触发trackRefValue
进行依赖收集。 - 在依赖触发时,需要谨记,先触发
computed
的effect
,再触发非computed
的effect
,为的是多次.value
赋值时造成死循环。