Effect
# 目录
# effects
通过 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
}
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 // 交给调度器时是否允许递归触发
}
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
}
}
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
这里有几个问题比较值得注意:
- 这里!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
}
}
2
3
4
5
6
7
8
9
需要注意的是,这里将 effect 已经清理过了,也就是说 stop 之后,在 trigger effect 时,应该就不存在已经关闭的 effect 了。
- effectStack 有什么作用?
effectStack 中缓存正在执行的 effect,以保证同一个 effect 不会被重复的消费。
# track
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
})
}
}
}
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
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'
}
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
}
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)
})
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
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)发生变化时,触发追踪目标上的所有的需要做更新的依赖进行执行和更新。