跳转到内容

effect 的实现

effect 是响应式中的核心函数,后续很多功能也是基于 effect 实现,例如:watchcomputed 等等。

import { reactive, effect } from '../dist/reactivity.esm.js'
const state = reactive({ count: 1 })
effect(() => {
console.log(state.count)
})
state.count++

分析这个代码,effect 里面的函数,会执行一次,然后会收集依赖,当依赖变化时,会重新执行函数。

首先把 effect 函数创建出来。

export interface ReactiveEffect<T = any> {
(): T // 代表没有参数,返回值为 T 的函数
}
// 当前正在执行的 effect
let activeEffect: ReactiveEffect | undefined
export function effect<T = any>(fn: () => T) {
// 当 effect 执行时,将其设置为当前激活的副作用函数
activeEffect = fn
// 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
fn()
// 当 effect 执行完成后,将其设置为 undefined
activeEffect = undefined
}
  • 创建了 activeEffect 变量,用来存储当前正在执行的 effect
  • 当 effect 执行时,将其设置为当前激活的副作用函数
  • 当 effect 执行完成后,将其设置为 undefined
  • fn 在执行的过程中,就会收集依赖

按照之前的响应式结构设计,对 track 进行修改。

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
// 当前正在执行的 effect
let activeEffect: ReactiveEffect | undefined
const targetMap = new WeakMap<any, KeyToDepMap>()
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 暂停依赖收集开关,没有activeEffect或者shouldTrack为false时,不进行依赖收集
if (!shouldTrack || activeEffect === undefined) {
return
}
console.log(`%c依赖收集: target ${JSON.stringify(target)}${type}${String(key)}`, 'color: #f40')
// 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
// 最后将当前 effect 添加到 deps 中
deps.add(activeEffect!)
}

这样就实现了依赖收集

20260408150227

上图可以看到,最后触发了 trigger 函数,trigger 函数目前还没实现逻辑,按照下面的顺序处理就行了

  • 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
  • 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
  • 最后遍历 deps 中的所有 effect,执行它们
export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
console.log(`%c派发更新: target ${JSON.stringify(target)}${type}${String(key)}`, 'color: #0f0')
// 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
const depsMap = targetMap.get(target)
if (!depsMap) {
// 从未被追踪过,直接返回
return
}
// 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
const deps = depsMap.get(key)
if (!deps) {
return
}
// 最后遍历 deps 中的所有 effect,执行它们
deps.forEach(effect => effect())
}

20260408151626

目前还有些问题需要处理,例如分支切换,循环依赖等。

  • 分支切换问题:当依赖项变化时,需要根据新的依赖项,来清理旧的依赖项。
  • 循环依赖问题:当依赖项之间存在循环依赖时,需要避免无限循环。
const state = reactive({ count: 0 })
effect(() => {
effect(() => {
console.log('第二个 effect 执行', state.count)
})
console.log('第一个 effect 执行', state.count)
})
setTimeout(() => {
state.count++
}, 1000)

20260408172605

分析:

effect 执行,又遇到了 effect,因为这里代码是同步的,所以会继续执行第二个 effect,然后再执行第一个 effect,但是当 state.count 变化后,只执行了第二个 effect,没有执行第一个。

export function effect<T = any>(fn: () => T) {
// 当 effect 执行时,将其设置为当前激活的副作用函数
activeEffect = fn
// 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
fn()
// 当 effect 执行完成后,将其设置为 undefined
activeEffect = undefined
}

主要的就是上面在 fn 执行完毕后,又将 activeEffect 设置为 undefined,所以第二个 effect 会执行,但是第一个 effect 不会执行(因为当执行到第一个 effect 时,activeEffectundefined

// 用来存储当前正在执行的 effect 栈
const effectStack: ReactiveEffect[] = []
export function effect<T = any>(fn: () => T) {
// 当 effect 执行时,将其设置为当前激活的副作用函数
activeEffect = fn
// 在 fn 函数调用之前,将当前 effect 压入栈顶
effectStack.push(fn)
// 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
fn()
// 在 fn 函数调用之后,将当前 effect 从栈顶弹出
effectStack.pop()
// 当 effect 执行完成后,将其设置为 undefined
activeEffect = undefined
// 恢复 activeEffect 为栈顶的 effect
activeEffect = effectStack[effectStack.length - 1]
}
effect(() => {
effect(() => {
console.log('第二个 effect 执行', state.count)
})
console.log('第一个 effect 执行', state.count)
})
setTimeout(() => {
console.log('1s 后,count 增加 1')
state.count++
}, 1000)

20260409133113

  • 定时器执行前
  1. 第二个 effect 执行
  2. 第一个 effect 执行
  • 定时器执行后

state.count++ 会触发 trigger 执行

  1. 第二个 effect 执行 -> ✅ 栈顶是第二个 effect
  2. 第二个 effect 执行 -> ✅ 此时栈顶是第一个 effect,顺序执行,就会打印这个
  3. 第一个 effect 执行 -> ✅ 代码依次执行
  4. 第二个 effect 执行 -> ❌ 此时已经执行完了,不应该再打印了

从目前执行顺序分析,最后一次打印的是有问题的,为什么会这样呢?

因为在 trigger 执行时,会遍历 deps 中的所有 effect

// 目前是直接这样执行的
deps.forEach(effect => effect())

但是由于在 effect 中,会不停的出栈入栈,会导致 deps 也会变化,所以要在 trigger 的时候创建一个副本,把可变的 deps 拷贝成一个“本轮稳定不变”的执行集合

export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
// console.log(`%c派发更新: target ${JSON.stringify(target)}【${type}】${String(key)}`, 'color: #0f0')
// 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
const depsMap = targetMap.get(target)
if (!depsMap) {
// 从未被追踪过,直接返回
return
}
// 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
const deps = depsMap.get(key)
if (!deps) {
return
}
// 最后遍历 deps 中的所有 effect,执行它们
deps.forEach(effect => effect())
// 执行effects中的副作用函数
const effects = new Set<ReactiveEffect>()
deps.forEach(effectFn => {
effects.add(effectFn)
})
effects.forEach(effect => effect())
}

20260409134939

这样的话就正常打印了

effect(() => {
console.log('effect 执行', state.count)
state.count++
})

20260409142010

直接就栈溢出了

可以把 state.count++ 改成 state.count = state.count + 1 ,在 effect 里面,又在收集依赖,又在触发更新,触发更新的时候又在收集依赖,就造成了无限循环

只需要在 trigger 中,判断一下,是否是当前正在执行的 effect,如果不是,才添加到 effects 中就行了

deps.forEach(effectFn => {
// 如果当前副作用函数不是当前激活的副作用函数,才添加到 effects 中
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})

20260409142621

const state = reactive({ count: 0, bar: 'bar' })
effect(() => {
console.log('---触发---')
for (const key in state) {
console.log(key)
}
})
state.bar = 'ba123r'

20260409150121

这个时候并没有生效,因为在 track 时,处理了 迭代属性,但是在 trigger 时,没有处理迭代属性,所以并没有生效

export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
// 之前代码
// 取得与ITERATE_KEY相关的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与ITERATE_KEY相关的副作用函数也添加到effects中
iterateEffects &&
iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})
effects.forEach(effect => effect())
}

20260410145705

还是没效果,说明代码没有执行到这里来。

export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
// 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
const depsMap = targetMap.get(target)
if (!depsMap) {
// 从未被追踪过,直接返回
return
}
// 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
const deps = depsMap.get(key)
if (!deps) {
return
}
// 执行effects中的副作用函数
const effects = new Set<ReactiveEffect>()
deps.forEach(effectFn => {
// 如果当前副作用函数不是当前激活的副作用函数,才添加到 effects 中
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})
// 取得与ITERATE_KEY相关的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与ITERATE_KEY相关的副作用函数也添加到effects中
iterateEffects &&
iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})
effects.forEach(effect => effect())
}

这里 deps 目前是 undefined,都没有走到 iterateEffects 这里来,修改下这里的代码就行了

export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
// 先根据 target 从 weakMap 中获取对应的 map,保存的是 key --- effects 的键值对
const depsMap = targetMap.get(target)
if (!depsMap) {
// 从未被追踪过,直接返回
return
}
// 再根据 key 从 depsMap 中获取对应的 deps,保存的是 effect 的集合
const deps = depsMap.get(key)
if (!deps) {
return
}
// 执行effects中的副作用函数
const effects = new Set<ReactiveEffect>()
deps &&
deps.forEach(effectFn => {
// 如果当前副作用函数不是当前激活的副作用函数,才添加到 effects 中
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})
// 取得与ITERATE_KEY相关的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
// 将与ITERATE_KEY相关的副作用函数也添加到effects中
iterateEffects &&
iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effects.add(effectFn)
}
})
effects.forEach(effect => effect())
}

20260410150307

这样就正常了。其实这里还应该考虑是新增还是修改,后面再处理。

<div id="app"></div>
<button id="flagBtn">update flag</button>
<button id="nameBtn">update name</button>
<button id="ageBtn">update age</button>
const state = reactive({
flag: false,
name: 'name',
age: 18,
})
effect(() => {
console.count('effect')
// 需要根据 flag 的值, 来决定 effect 的依赖项是什么,把不需要的依赖给清理掉
if (state.flag) {
app.innerHTML = state.name
} else {
app.innerHTML = state.age
}
})
flagBtn.onclick = () => {
state.flag = !state.flag
}
nameBtn.onclick = () => {
// 如果 flag 为 true,那么 name 就是依赖项,如果 flag 为 false,那么 name 就不是依赖项,需要清理掉
state.name = state.name + Math.random()
}
ageBtn.onclick = () => {
// 如果 flag 为 true,那么 age 就是依赖项,如果 flag 为 false,那么 age 就不是依赖项,需要清理掉
state.age++
}

20260413174847

也就是展示这样的内容,点击不同的按钮分别干不同的事情,在 trigger 中打印 targetMap

export function trigger(target: object, type: TriggerOpTypes, key: unknown) {
console.log('targetMap', targetMap)
}
  • 一开始 flagfalse,所以这里 effect 执行的时候收集到的依赖是 flagage

  • 点击修改下年龄,会触发 trigger 函数, 20260413175451 可以看到 targetMap 里面确实是收集的 flagage 的依赖。

  • 此时再点击一下 update flag,将 flag 改为 true,会触发 effect 重新执行。 20260413175559 修改后可以看到这里还是只收集到了 flagage 的依赖,这是明显不对的,因为此时 flagtrue,所以应该把 name 也收集到依赖项中。

    为什么这里没有收集到 name 的依赖项?

    因为在 trigger 函数中,最后执行的是 effects.forEach(effect => effect())。

    effects.forEach(effect => effect())

    也就是说:deps 里面存的是谁,trigger 重新执行的就是谁

    然后再看之前的 effect 的代码

    export function effect<T = any>(fn: () => T) {
    // 当 effect 执行时,将其设置为当前激活的副作用函数
    activeEffect = fn
    // 在 fn 函数调用之前,将当前 effect 压入栈顶
    effectStack.push(fn)
    // 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
    fn()
    // 在 fn 函数调用之后,将当前 effect 从栈顶弹出
    effectStack.pop()
    // 恢复 activeEffect 为栈顶的 effect
    activeEffect = effectStack[effectStack.length - 1]
    }

    核心内容是

    • activeEffect = fn
    • 所以 track 中收集进去的是 fn

    也就是说

    deps.add(fn)

    第一次执行时 当然是正常的,因为手动调用了 effect(fn)

    1. activeEffect = fn
    2. 执行 fn()
    3. 访问 state.flag
    4. track() 看到 activeEffectfn
    5. 成功收集依赖

    所以第一次是没问题的

    但点击 update flag 之后

    走的是 trigger 函数,而不是 effect 函数了。trigger 函数里面拿到的是原始函数 fn,所以就没有之前的这些步骤。

    • activeEffect = fn
    • effectStack.push(fn)
    • effectStack.pop()
    • 恢复栈顶

    所以这次重新执行时,fn 里面读到 state.flagstate.name 的时候:

    track()

    此时看到的 activeEffect 会是 undefined。那么就会到导致 name 不会被收集到依赖里面,最后的 targetWeakMap 里面只有 flagage 的依赖。

export function effect<T = any>(fn: () => T) {
// 当 effect 执行时,将其设置为当前激活的副作用函数
activeEffect = fn
// 在 fn 函数调用之前,将当前 effect 压入栈顶
effectStack.push(fn)
// 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
fn()
// 在 fn 函数调用之后,将当前 effect 从栈顶弹出
effectStack.pop()
// 恢复 activeEffect 为栈顶的 effect
activeEffect = effectStack[effectStack.length - 1]
const effectFn = () => {
// 当effectFn执行时,将其设置为当前激活的副作用函数,这样在 `track` 中收集进去的是 `effectFn`,trigger 重新执行的就是 `effectFn`,就可以拿到上下文了。
activeEffect = effectFn
// 在 fn 函数调用之前,将当前 effect 压入栈顶
effectStack.push(fn)
// 执行 fn 函数,在 fn 执行的过程中,会收集到对应的依赖
fn()
// 在调用副作用函数之后,将其弹出effectStack栈
effectStack.pop()
// activeEffect始终指向当前effectStack栈顶的副作用函数
activeEffect = effectStack[effectStack.length - 1]
}
effectFn()
}

20260414100714

修改 effect 后再点击一次 update flag,可以看到此时 targetMap 里面收集到了 flag, name, age 的依赖。