实现 computed,并梳理它作为 Dep、Sub 以及脏检查的工作方式
一个最简单的使用:
const count = ref(0)
const c = computed(() => count.value + 1)
effect(() => {
c.value
})
count.value++
computed 做了什么呢
双重身份:既是 Dep,也是 Sub
computed 它同时扮演两种角色。
Dep:作为依赖项,被其他响应式副作用订阅Sub:作为订阅者,在执行自己的getter时收集依赖
computed 在求值时会读取 count.value,所以它是 Sub;而 effect 又会读取 c.value,所以 computed 自己也会变成
Dep。

理解了这一点后,后面的实现思路就比较清楚了。
先实现一个基础版 computed
在使用时,computed 有两种写法:
const c = computed(() => count.value)
computed({
get() {
},
set() {
}
})
所以可以先写一个基础签名:
import type { Link, ReactiveNode } from './system'
import { isFunction } from '@vue/shared'
import { ReactiveFlags } from './constant'
class ComputedRefImpl<T = any> implements ReactiveNode {
[ReactiveFlags.IS_REF] = true
subs?: Link
subsTail?: Link
deps?: Link
depsTail?: Link
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined
) {
}
_value: T | undefined
get value() {
this._value = this.fn()
return this._value
}
set value(newValue: T) {
if (this.setter) {
this.setter(newValue)
}
else {
console.warn('Write operation failed: computed value is readonly')
}
}
}
export type ComputedGetter<T> = (oldValue?: T) => T
export type ComputedSetter<T> = (newValue: T) => void
export interface WritableComputedOptions<T, S = T> {
get: ComputedGetter<T>
set: ComputedSetter<S>
}
export function computed<T>(getter: ComputedGetter<T>)
export function computed<T, S = T>(options: WritableComputedOptions<T, S>)
export function computed<T = any>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = () => {
}
}
else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(getter, setter)
}
这时可以先写一个最基础的测试:
it('computed ', () => {
const count = ref(0)
const c = computed(() => count.value + 1)
effect(() => {
console.log('c.value', c.value)
})
count.value = 1
})
到这一步,代码已经能工作,但它还不是一个真正意义上的
computed。
因为现在每次访问 .value 都会直接执行 getter,还没有实现惰性求值。
让 computed 作为 Dep
effect 读取 c.value 时,应该把当前副作用和 computed 建立关联。
也就是说,computed 要先具备 Dep 的能力。
可以在 get value() 中完成依赖收集:
import { activeSub } from './effect'
import { link } from './system'
get
value()
{
this._value = this.fn()
if (activeSub) {
link(this, activeSub)
}
return this._value
}
这样一来,谁读取了 computed.value,谁就会订阅这个 computed。

让 computed 作为 Sub
只做到上一步还不够,因为 computed 自己也要去订阅别的响应式数据。
它在执行 getter 的过程中,会访问到像 count.value 这样的响应式值,所以它也必须像 effect 一样,在执行期间完成依赖收集。
get
value()
{
this.update()
if (activeSub) {
link(this, activeSub)
}
return this._value
}
update()
{
const prevSub = activeSub
setActiveSub(this)
startTrack(this)
try {
this._value = this.fn()
}
finally {
endTrack(this)
setActiveSub(prevSub)
}
}
这样在 update() 执行期间,computed 就会作为当前活跃的订阅者,把 getter 中访问到的响应式数据都收集起来。

为什么会报错
改完之后,执行过程中会出现一个错误:

问题出在 propagate。
原本我们默认 subs 链表中的节点都是普通副作用,所以会直接调用 notify:
export function propagate(subs: Link) {
const queuedEffect = []
let linkNode = subs
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking) {
queuedEffect.push(sub)
}
linkNode = linkNode.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
但 computed 并没有 notify,它是通过 update 驱动重新计算的。
所以这里要把 computed 和普通 effect 区分开:
export function propagate(subs: Link) {
const queuedEffect = []
let linkNode = subs
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking) {
if ('update' in sub) {
// 如果存在 update 方法,说明这里是 computed
processComputedUpdate(sub as ComputedRefImpl)
}
else {
queuedEffect.push(sub)
}
}
linkNode = linkNode.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
function processComputedUpdate(sub) {
sub.update() // 执行计算属性
propagate(sub.deps) // 通知 subs 链表上的所有 sub 重新执行
}
到这里,结构上已经能跑通了,但测试还是没通过。
还需要 dirty 脏检查
问题的根源在于:当前实现里,每次读取 .value 都会触发 update()。
get
value()
{
this.update()
// ...
}
这意味着它根本没有缓存能力,也没有惰性。
所以要引入一个 dirty 标记,只有值变脏了才重新计算。
不过还有一个细节:即使依赖发生变化,也不代表 computed 的结果一定变化。
比如:
const c = computed(() => count * 0)
无论 count 怎么变,结果始终都是 0。这时如果还继续无脑触发下游更新,就会产生不必要的执行。
所以可以换一个思路:先把副作用标记为脏,再决定是否需要继续派发。
先在副作用上增加一个脏标记:
export class ReactiveEffect {
// ...
dirty = false // 是否需要重新计算
}
然后在更新和结束追踪时加入脏检查逻辑:
// system.ts
export function propagate(subs: Link): void {
// ...
if (!sub.tracking && !sub.dirty) {
sub.dirty = true
if ('update' in sub) {
processComputedUpdate(sub as ComputedRefImpl)
}
else {
queuedEffect.push(sub)
}
}
}
function processComputedUpdate(sub: ComputedRefImpl) {
if (sub.subs && sub.update()) {
sub.update()
propagate(sub.subs)
}
}
export function endTack(sub: ReactiveNode) {
sub.tracking = false
const depsTail = sub.depsTail
sub.dirty = false // 执行完毕后恢复为干净状态
// ...
}
这样 computed 才真正具备了两个关键特性:
- 惰性求值:只有访问时才计算
- 缓存能力:依赖没变脏时直接复用上一次结果
完整代码
// computed.ts
import { hasChanged, isFunction } from '@vue/shared'
import { ReactiveFlags } from './constant'
import { activeSub, setActiveSub } from './effect'
import { endTack, link, Link, ReactiveNode, startTrack } from './system'
export type ComputedGetter<T> = (oldValue?: T) => T
export type ComputedSetter<T> = (newValue: T) => void
export interface WritableComputedOptions<T, S = T> {
get: ComputedGetter<T>
set: ComputedSetter<S>
}
export class ComputedRefImpl<T = any> implements ReactiveNode {
[ReactiveFlags.IS_REF] = true
_value: T
subs: Link
subsTail: Link
deps: Link | undefined
depsTail: Link | undefined
tracking = false
dirty = true // 计算属性脏不脏
constructor(
public fn: ComputedGetter<T>,
private readonly setter: ComputedSetter<T> | undefined
) {}
get value() {
if (this.dirty) {
this.update()
this.dirty = false
}
if (activeSub) {
link(this, activeSub)
}
return this._value
}
set value(newValue: T) {
if (this.setter) {
this.setter(newValue)
}
else {
console.warn('Write operation failed: computed value is readonly')
}
}
update() {
const prevSub = activeSub
setActiveSub(this)
startTrack(this)
try {
const oldValue = this._value
this._value = this.fn()
return hasChanged(this._value, oldValue)
}
finally {
endTack(this)
// 执行完成, 恢复之前的 effect
setActiveSub(prevSub)
}
}
}
export function computed<T>(getter: ComputedGetter<T>)
export function computed<T, S = T>(options: WritableComputedOptions<T, S>)
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
}
else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter)
return cRef as any
}
// system.ts
export function propagate(subs: Link): void {
let linkNode = subs
const queuedEffect = []
while (linkNode) {
const sub = linkNode.sub
if (!sub.tracking && !sub.dirty) {
sub.dirty = true
if ('update' in sub) {
// computed
processComputedUpdate(sub as ComputedRefImpl)
}
else {
queuedEffect.push(sub)
}
}
linkNode = linkNode.nextSub
}
queuedEffect.forEach(effect => effect.notify())
}
function processComputedUpdate(sub: ComputedRefImpl) {
if (sub.subs && sub.update()) {
sub.update()
propagate(sub.subs)
}
}