effect 的实现
effect 是响应式中的核心函数,后续很多功能也是基于 effect 实现,例如:watch、computed 等等。
effect 基础实现
Section titled “effect 基础实现”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 的函数}
// 当前正在执行的 effectlet 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 函数 依赖收集
Section titled “track 函数 依赖收集”按照之前的响应式结构设计,对 track 进行修改。
type Dep = Set<ReactiveEffect>type KeyToDepMap = Map<any, Dep>
// 当前正在执行的 effectlet 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!)}这样就实现了依赖收集

trigger 函数 触发依赖更新
Section titled “trigger 函数 触发依赖更新”上图可以看到,最后触发了 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())}
目前还有些问题需要处理,例如分支切换,循环依赖等。
- 分支切换问题:当依赖项变化时,需要根据新的依赖项,来清理旧的依赖项。
- 循环依赖问题:当依赖项之间存在循环依赖时,需要避免无限循环。
effect 嵌套问题
Section titled “effect 嵌套问题”const state = reactive({ count: 0 })
effect(() => { effect(() => { console.log('第二个 effect 执行', state.count) }) console.log('第一个 effect 执行', state.count)})
setTimeout(() => { state.count++}, 1000)
分析:
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 时,activeEffect 为 undefined)
// 用来存储当前正在执行的 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)
- 定时器执行前
- 第二个 effect 执行
- 第一个 effect 执行
- 定时器执行后
state.count++ 会触发 trigger 执行
- 第二个 effect 执行 -> ✅ 栈顶是第二个 effect
- 第二个 effect 执行 -> ✅ 此时栈顶是第一个 effect,顺序执行,就会打印这个
- 第一个 effect 执行 -> ✅ 代码依次执行
- 第二个 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())}
这样的话就正常打印了
无限循环问题
Section titled “无限循环问题”effect(() => { console.log('effect 执行', state.count) state.count++})
直接就栈溢出了
可以把 state.count++ 改成 state.count = state.count + 1 ,在 effect 里面,又在收集依赖,又在触发更新,触发更新的时候又在收集依赖,就造成了无限循环
只需要在 trigger 中,判断一下,是否是当前正在执行的 effect,如果不是,才添加到 effects 中就行了
deps.forEach(effectFn => { // 如果当前副作用函数不是当前激活的副作用函数,才添加到 effects 中 if (effectFn !== activeEffect) { effects.add(effectFn) }})
for…in 循环的考虑
Section titled “for…in 循环的考虑”const state = reactive({ count: 0, bar: 'bar' })
effect(() => { console.log('---触发---') for (const key in state) { console.log(key) }})
state.bar = 'ba123r'
这个时候并没有生效,因为在 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())}
还是没效果,说明代码没有执行到这里来。
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())}
这样就正常了。其实这里还应该考虑是新增还是修改,后面再处理。
分支切换问题
Section titled “分支切换问题”<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++}
也就是展示这样的内容,点击不同的按钮分别干不同的事情,在 trigger 中打印 targetMap。
export function trigger(target: object, type: TriggerOpTypes, key: unknown) { console.log('targetMap', targetMap)}-
一开始
flag为false,所以这里effect执行的时候收集到的依赖是flag和age -
点击修改下年龄,会触发
trigger函数,
可以看到 targetMap里面确实是收集的flag和age的依赖。 -
此时再点击一下
update flag,将flag改为true,会触发effect重新执行。
修改后可以看到这里还是只收集到了 flag和age的依赖,这是明显不对的,因为此时flag为true,所以应该把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 为栈顶的 effectactiveEffect = effectStack[effectStack.length - 1]}核心内容是
activeEffect = fn- 所以
track中收集进去的是fn
也就是说
deps.add(fn)第一次执行时 当然是正常的,因为手动调用了
effect(fn):activeEffect = fn- 执行
fn() - 访问
state.flag track()看到activeEffect是fn- 成功收集依赖
所以第一次是没问题的
但点击
update flag之后走的是
trigger函数,而不是effect函数了。trigger函数里面拿到的是原始函数fn,所以就没有之前的这些步骤。activeEffect = fneffectStack.push(fn)effectStack.pop()- 恢复栈顶
所以这次重新执行时,
fn里面读到state.flag、state.name的时候:track()此时看到的
activeEffect会是undefined。那么就会到导致name不会被收集到依赖里面,最后的targetWeakMap里面只有flag和age的依赖。
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()}
修改 effect 后再点击一次 update flag,可以看到此时 targetMap 里面收集到了 flag, name, age 的依赖。