Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • reactivity

    • 开始上手
    • Ref
    • Reactive
    • Handler
    • Effect
      • 目录
      • effects
      • track
      • trigger
      • track 的暂停与恢复
      • 一些问题
      • 文章小结
    • Computed
  • runtime-core

  • runtime-dom

  • vue3
  • reactivity
jonsam
2022-04-14
目录

Effect

# 目录

  • 目录
  • effects
  • track
  • trigger
  • track 的暂停与恢复
  • 一些问题
    • effect 流程
  • 文章小结

# effects

标签: 生产 effect

通过 effect 函数创建 effect,可以将 fn (回调) 包装成一个 effect 对象。

export function effect<T = any>(
  fn: () => T, // 创建 effect 的回调函数
  options: ReactiveEffectOptions = EMPTY_OBJ // 创建 effect 的配置项
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    // 如果已经是一个 effect 对象,则以 raw fn 重新创建 effect
    fn = fn.raw
  }
  // 用回调函数和配置项创建 effect
  const effect = createReactiveEffect(fn, options)
  // 除 lazy effect 之外都应该立即执行一遍
  // 为什么在创建完 effect 要执行一遍?初始化响应式的代码逻辑
  if (!options.lazy) {
    effect()
  }
  return effect
}
export function isEffect(fn: any): fn is ReactiveEffect {
  // 通过 _isEffect 属性来判断是否是 effect 对象
  return fn && fn._isEffect === true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个函数的核心是调用 createReactiveEffect 工厂函数创建 effect。

先来看看什么是 effect:

export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true // 是否是 effect 对象的标志,isEffect 判断的根据
  id: number // effect id
  active: boolean // 是否处于激活状态,没有被 stop 的 effect 都属于激活状态
  raw: () => T // 原本的回调,就是被包装的回调函数
  deps: Array<Dep> // 当前 effect 所属于的依赖数组,结构为 Array<Set>,每个 Set 代表 Map<key, effect>
  options: ReactiveEffectOptions // 当前 effect 的配置项
  allowRecurse: boolean // 是否允许递归
}
export interface ReactiveEffectOptions {
  lazy?: boolean // 是否懒响应,懒响应的 effect 创建时不执行
  scheduler?: (job: ReactiveEffect) => void // effect 的调度器,如果有调度器,在执行 effect 时会交由调度器处理
  onTrack?: (event: DebuggerEvent) => void // 用于 dev 环境,跟踪 track 过程,track 执行时触发
  onTrigger?: (event: DebuggerEvent) => void // 用于 dev 环境,跟踪 trigger 过程,run effect 时触发
  onStop?: () => void // stop effect 时触发
  allowRecurse?: boolean // 交给调度器时是否允许递归触发
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

重点来看下 createReactiveEffect 函数:

// 当前真该执行的 effect 的栈
const effectStack: ReactiveEffect[] = []
function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 根据 fn 创建一个 effect,effect 本质上是一个 function,是可以执行的
  const effect = function reactiveEffect(): unknown {
    // 注意:此函数在 effect 执行时调用,effect的属性是有值的
    // 如果当前 effect 已经被 stop 了,就执行他
    // 如果是 scheduled effect,即时 stop 了也执行,但是不开启 track
    // effect 就是指当前的 effect 函数
    if (!effect.active) {
      return fn()
    }
    // 如果 effect 执行栈里不包含当前的 effect,即当前的 effect 并没有正在执行
    if (!effectStack.includes(effect)) {
      // 先清空依赖集合中的当前的 effect,因为此 effect 即将被消费。
      // 这表明 effect 消费以后就被删除了,新的 effect 会被加入
      cleanup(effect)
      try {
        // 开启依赖追踪
        enableTracking()
        // 将 effect 推进执行栈
        effectStack.push(effect)
        // 将当前 effect 设置为正在执行的 effect
        activeEffect = effect
        // 执行回调并返回,后面的代码不在执行
        return fn()
      } finally {
        // 如果 fn 执行报错就从执行栈中弹出 effect
        // 执行错误的 effect 将会被舍弃
        effectStack.pop()
        // 重置 track 的状态
        resetTracking()
        // activeEffect 回退指向栈顶的 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  // 是否允许递归
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  // effect 默认是激活的
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

function cleanup(effect: ReactiveEffect) {
  // 取出当前 effect 所属的依赖数组
  const { deps } = effect
  if (deps.length) {
    // 循环对应每个 key 值的依赖集合
    for (let i = 0; i < deps.length; i++) {
      // 从各个集合中将此 effect 清除掉
      deps[i].delete(effect)
    }
    // 依赖数组清空:注意这只是 effect.deps,不影响整个依赖关系
    deps.length = 0
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

这里有几个问题比较值得注意:

  1. 这里!effect.active 的 effect 为什么要执行?

stop 函数可以将 effect.active 关闭,并且触发 onStop 钩子函数。

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false
  }
}
1
2
3
4
5
6
7
8
9

需要注意的是,这里将 effect 已经清理过了,也就是说 stop 之后,在 trigger effect 时,应该就不存在已经关闭的 effect 了。

  1. effectStack 有什么作用?

effectStack 中缓存正在执行的 effect,以保证同一个 effect 不会被重复的消费。

# track

标签: 依赖收集 | 收集 effect

track 的主要作用是针对 target 进行依赖收集,track 是响应式的基础。

// (副作用)依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 这里的 type 在 prod 没有作用
  // shouldTrack  可以关闭依赖追踪,activeEffect === undefined 表示还没有创建过 effect
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  // 取出当前 target 的依赖集合,结构:Map<target, Map<key, Set>>,
  // targetMap → depsMap → dep → activeEffect
  let depsMap = targetMap.get(target)
  // 依赖集合不存在就将之初始化
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 将当前的 effect 进行收集,并且 activeEffect.deps 数组记录自身所属于的依赖集合(dep)
  // activeEffect 表示当前正在执行的 effect
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  • 退出 track 的条件是:track 是 enable 的,且已经创建过 effect。
  • depsMap 依赖收集的结构是: Map<target, Map<key, Set>> ,map-map-set 结构。

# trigger

标签: 消费 effect

trigger 的主要作用是消费 target 上需要消费的 effect (副作用),trigger 是响应式的核心。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 取出 target 上的依赖集合,结构:Map<target, Map<key, Set>>,
  const depsMap = targetMap.get(target)
  // 没有依赖则不必消费依赖了
  if (!depsMap) {
    // never been tracked
    return
  }
  // 待消费的依赖集合
  const effects = new Set<ReactiveEffect>()
  // add 函数将 key set 中的 effect 推进 effects 数组。
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        // 如果 effect 不是栈顶的 effect 且允许递归
        if (effect !== activeEffect || effect.allowRecurse) {
          // 将依赖添加到待消费的依赖集合
          effects.add(effect)
        }
      })
    }
  }

  // 如果是 CLEAR 类型
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    // Map 在 forEach 中获取的是 values,此处传入的是 Set
    // 触发 target 上所有的 effect
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 如果 target 是一个数组
    // 数组依赖的消费
    depsMap.forEach((dep, key) => {
      // key >= newValue 表示 大于 newValue 的 key set 将会被消费
      if (key === 'length' || key >= (newValue as number)) {
        // 只有满足上述条件的 set 才可以被消费
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    // void 0 returns undefined and can not be overwritten while undefined can be overwritten.
    //  see https://stackoverflow.com/questions/7452341/what-does-void-0-mean
    // 如果 key !=== undefined,则消费 key 所对应的 Set。
    // 消费某个单独的 key set
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    // iteration key 需要消费的 effect
    switch (type) {
      // 增加属性
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          // 根据 target 类型不同,选择需要消费的 effect 集合
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          // 数组增加值改变 length
          add(depsMap.get('length'))
        }
        break
      // 删除属性
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        // TODO 为什么不处理数组的 length
        break
      // 修改属性
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 执行 effect,指定了 scheduler 的交给 scheduler 处理。
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  // 执行待消费的所有的依赖
  effects.forEach(run)
}

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  • trigger 的类型包括:SET、ADD、DELETE、CLEAR。
  • 需要注意的是: effect 消费完之后就被删除,新的 effect 将会被生产,因为响应式的要求就是在每一次响应中 trigger effects 。effect 在 effect () 函数本身中被 cleanUp。

# track 的暂停与恢复

下面我们来看看管理 track 暂停与恢复的机制:

let shouldTrack = true
// 保存上一次的 shouldTrack 状态
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • shouldTrack 是当前的 track 的开关。
  • trackStack 是 track 状态的栈,用于快速恢复上一次 track 的状态,便于在执行错误等情况下进行恢复。

# 一些问题

# effect 流程

在 track 中我们注意到将 activeEffect 加入依赖集合,activeEffect 表示正在被执行的 effect,可是 trigger 不是应该在 track 之后吗?如果说 effect 是在初始化创建时给 activeEffect 赋值的,那么 activeEffect 怎么能保证就是当前需要被加入的 effect 呢?

为了弄清楚这个问题,我们来做个测试:

it('should observe basic properties', () => {
  let dummy
  let temp
  const counter = reactive({ num: 0 })
  console.log('==>', 1)
  effect(() => (dummy = counter.num))
  console.log('==>', 2)
  effect(() => (temp = counter.num * 2))
  console.log('==>', 3)

  expect(dummy).toBe(0)
  console.log('==>', 4)
  expect(counter.num).toBe(0)
  console.log('==>', 5)
  counter.num = 7
  console.log('==>', 6)
  expect(dummy).toBe(7)
  console.log('==>', 7)
  expect(temp).toBe(14)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

执行上述 jest 代码段,结果如下:

// 创建 reactive 对象,target 被 proxy
==> createReactiveObject { target: { num: 0 } }

==> 1
// 创建 target 的 effect 对象
==> createReactiveEffect [Arguments] { '0': [Function], '1': {} }
// 创建完之后需要初始化,执行 effect,触发 get handler
==> get { target: { num: 0 }, key: 'num' }
// get 时触发 track 手机依赖,结构 Map<{ num: 0 }, Map<num, Set<effect>>>,将 effect 对象放在 Set 中
// 注意:因为初始化时一定会触发 get 进行触发 track,所以此时 activeEffect 一定是正需要收集的 effect
==> track [Arguments] { '0': { num: 0 }, '1': 'get', '2': 'num' } { activeEffect:
   { [Function: reactiveEffect]
     id: 0,
     allowRecurse: false,
     _isEffect: true,
     active: true,
     raw: [Function],
     deps: [],
     options: {} } }

==> 2
// 创建一个新的 effect
==> createReactiveEffect [Arguments] { '0': [Function], '1': {} }
// 初始化时触发 get
==> get { target: { num: 0 }, key: 'num' }
// get handler 中追踪依赖,activeEffect 仍然可以保持正确
==> track [Arguments] { '0': { num: 0 }, '1': 'get', '2': 'num' } { activeEffect:
   { [Function: reactiveEffect]
     id: 1,
     allowRecurse: false,
     _isEffect: true,
     active: true,
     raw: [Function],
     deps: [],
     options: {} } }

==> 3
// 非响应式的数据不会响应
==> 4
// 获取响应式数据触发 get handler
==> get { target: { num: 0 }, key: 'num' }
// 追踪依赖,此时没有正在创建的 effect
==> track [Arguments] { '0': { num: 0 }, '1': 'get', '2': 'num' } { activeEffect: undefined }

==> 5
// 设置响应式数据触发 set handler
==> set { target: { num: 0 }, key: 'num' }
// 传入的 value 需要 toRaw,触发 get handler,key 为 __v_raw,builtin 属性不需要 track
==> get { target: { num: 7 }, key: '__v_raw' }
// set handler 触发 trigger,key 为 number,newValue 为 7
// depsMap.get(target).get(key) 找到 effect,并执行
==> trigger [Arguments] { '0': { num: 7 }, '1': 'set', '2': 'num', '3': 7, '4': 0 }
// 计算响应值触发 get handler,计算dummy
==> get { target: { num: 7 }, key: 'num' }
// 追踪依赖
// 重新计算响应值时重新追踪了依赖,所以 effect 在消费之后才可以 cleanup
==> track [Arguments] { '0': { num: 7 }, '1': 'get', '2': 'num' } { activeEffect:
   { [Function: reactiveEffect]
     id: 0,
     allowRecurse: false,
     _isEffect: true,
     active: true,
     raw: [Function],
     deps: [],
     options: {} } }
// 计算响应值触发 get handler,计算 temp
==> get { target: { num: 7 }, key: 'num' }
// 追踪依赖
==> track [Arguments] { '0': { num: 7 }, '1': 'get', '2': 'num' } { activeEffect:
   { [Function: reactiveEffect]
     id: 1,
     allowRecurse: false,
     _isEffect: true,
     active: true,
     raw: [Function],
     deps: [],
     options: {} } }

==> 6
// 非响应式数据不响应

==> 7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

通过上面的测试代码,我们应该对 effect 生产、追踪和消费的过程以及响应式的原理比较清晰了。需要注意的有一下几点:

  • effect 被生产之后需要被初始化,也就是 effect 被执行一次(lazy effect 除外),因此在初始化计算响应式值的过程中会触发 get handler,进而会对创建的 effect 进行追踪(track)。这也就是 activeEffect 能保持是需要被 track 的 effect 的原因。由此也可说明, activeEffect 实际指的是 正在被创建的 effect 。
  • effect 被消费之后之所以可以被 cleanup,原因是因为在 effect 被消费时执行 effect,会重新计算响应值,在这个过程中会重新触发 get handler,进而对 effect 进行 track,重新收集依赖。

# 文章小结

这篇文章分析了 effect 的生产、追踪和消费的原理,以及 track 的暂停与恢复等细节问题。我们已经介绍了 ref、reactive 等响应式 api,而本篇所讲的 effect 就是响应式 api 响应式功能的核心。总体来说,effect 的核心就是 track 和 trigger,也就是 依赖追踪 和 依赖触发 。依赖追踪就是对追踪目标 target 上的一系列的 key 所产生的依赖关系(相应回调)进行收集,依赖触发就是在数据(target)发生变化时,触发追踪目标上的所有的需要做更新的依赖进行执行和更新。

编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
Handler
Computed

← Handler Computed→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式