1、副作用函数
副作用函数是指会产生副作用的函数,如下面的代码所示:
js
function effect(){
document.body.innerText = 'hello vue3'
}
当 effect
函数执行时,它会设置 body
的文本内容,但除了 effect
函数之外的任何函数都可以读取或设置 body
的文本内容。也就是说,effect
函数的执行会直接或间接影响其他函数的执行,这时我们说 effect
函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用。
js
// 全局变量
let val = 1
function effect() {
val = 2 // 修改全局变量,产生副作用
}
2、副作用函数的全局变量
在副作用模块中,定义了几个全局的变量,提前认识这些变量有助与我们了解副作用函数的生成以及调用的过程。
js
// packages/reactivity/src/effect.ts
export type Dep = Set<ReactiveEffect> & TrackedMarkers
type KeyToDepMap = Map<any, Dep>
// WeakMap 集合存储副作用函数
const targetMap = new WeakMap<any, KeyToDepMap>()
// 用一个全局变量存储当前激活的 effect 函数
export let activeEffect: ReactiveEffect | undefined
// 标识是否开启了依赖收集
export let shouldTrack = true
const trackStack: boolean[] = []
targetMap
targetMap
是一个 WeakMap
类型的集合,用来存储副作用函数,从类型定义可以看出 targetMap
的数据结构方式:
- WeakMap 由
target --> Map
构成- key: 原始对象 target
- value: Map 实例
- Map 由
key --> Set
构成- key: 原始对象 target 的 key
- value: 副作用函数组成的 Set
其中 WeakMap
的键是原始对象 target ,WeakMap
的值是一个 Map
实例,Map
的键是原始对象 target
的 key
,Map
的值是一个由副作用函数组成的 Set
。它们的关系如下:
targetMap 为什么使用 WeakMap
我们来看下面的代码:
js
const map = new Map();
const weakMap = new WeakMap();
(function() {
const foo = {foo: 1};
const bar = {bar: 2};
map.set(foo, 1); // foo 对象是 map 的key
weakMap.set(bar, 2); // bar 对象是 weakMap 的 key
})
在上面的代码中,定义了 map
和 weakMap
常量,分别对应 Map
和 WeakMap
的实例。在立即执行的函数表达式内部定义了两个对象:foo
和 bar
,这两个对象分别作为 map
和 weakMap
的key。
当函数表达式执行完毕后,对于对象 foo
来说,它仍然作为 map
的 key 被引用着,因此垃圾回收器 不会把它从内存中移除,我们仍然可以通过 map.keys 打印出对象 foo
。
对于对象 bar
来说,由于 WeakMap
的 key 是弱引用,它不影响垃圾收集器的工作,所以一旦表达式执行完毕,垃圾回收器就会把对象 bar
从内存中移除,并且我们无法获取 weakMap
的 key
值,也就无法通过 weakMap
取得对象 bar
。
简单地说,WeakMap
对 key
是弱引用
,不影响垃圾回收器的工作。根据这个特性可知,一旦 key
被垃圾回收器回收,那么对应的键和值就访问不到了。所以 WeakMap
经常用于存储那些只有当 key
所引用的对象存在时 (没有被回收) 才有价值的信息。
例如在上面的场景中,如果 target
对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map
来代替 WeakMap
,那么即使用户侧的代码对 target
没有任何引用,这个 target
也不会被回收,最终可能导致内存溢出。
activeEffect
activeEffect
变量用来维护当前正在执行的副作用
shouldTrack
shouldTrack
变量用来标识是否开启依赖搜集,只有 shouldTrack
的值为 true 时,才进行依赖收集,即将副作用函数添加到依赖集合中。
3、副作用的实现
effect 函数
effect API 用来创建一个副作用函数,接受两个参数,分别是用户自定义的fn函数和options 选项。源码如下所示:
js
// packages/reactivity/src/effect.ts
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 当传入的 fn 中存在 effect 副作用时,将这个副作用的原始函数赋值给 fn
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 创建一个副作用
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 如果不是延迟执行的,则立即执行一次副作用函数
if (!options || !options.lazy) {
_effect.run()
}
// 通过 bind 函数返回一个新的副作用函数
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
// 将副作用添加到新的副作用函数上
runner.effect = _effect
// 返回这个新的副作用函数
return runner
}
由上面的代码可以知道,当传入的参数 fn
中存在 effect
副作用时,将这个副作用的原始函数赋值给 fn
。然后调用 ReactiveEffect
类创建一个封装后的副作用函数。
在有些场景下,我们不希望 effect
立即执行,而是希望它在需要的时候才执行,我们可以通过在 options 中添加 lazy 属性来达到目的。在 effect
函数源码中,判断 options.lazy
选项的值,当值为 true
时,则不立即执行副作用函数,从而实现懒执行的 effect
。
接着通过 bind
函数返回一个新的副作用函数 runner
,这个新函数的this被指定为 _effect
,并将 _effect
添加到这个新副作用函数的 effect
属性上,最后返回这个新副作用函数。
由于 effect API
返回的是封装后的副作用函数,原始的副作用函数存储在封装后的副作用函数的effect
属性上,因此如果想要获取用户传入的副作用函数,需要通过 fn.effect.fn 来获取。
在 effect
函数中调用了 ReactiveEffect
类创建副作用,接下来看看 ReactiveEffect
类的实现。
ReactiveEffect 类
js
// packages/reactivity/src/effect.ts
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run() {
// 如果 effect 已停用,返回原始副作用函数执行后的结果
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
// 创建一个新的副作用前将当前正在执行的副作用存储到新建的副作用的 parent 属性上,解决嵌套effect 的情况
this.parent = activeEffect
// 将创建的副作用设置为当前正则正在执行的副作用
activeEffect = this
// 将 shouldTrack 设置为 true,表示开启依赖收集
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
// 初始化依赖
initDepMarkers(this)
} else {
// 清除依赖
cleanupEffect(this)
}
// 返回原始副作用函数执行后的结果
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
// 使用 effectTrackDepth 变量记录当前的依赖追踪深度。然后使用位运算符 << 将数字 1 左移 effectTrackDepth 位,生成一个新的依赖追踪标识符。这个标识符是一个二进制数,其中每个二进制位表示一个响应式数据的依赖关系
trackOpBit = 1 << --effectTrackDepth
// 重置当前正在执行的副作用
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
}
}
// 停止(清除) effect
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
在 ReactiveEffect
类中,定义了一个 run
方法,这个 run
方法就是创建副作用时实际运行方法。每次派发更新时,都会执行这个 run
方法,从而更新值。
全局变量 activeEffect
用来维护当前正在执行的副作用 ,当存在嵌套渲染组件的时候,依赖收集后,副作用函数会被覆盖,即 activeEffect
存储的副作用函数在嵌套 effect
的时候会被内层的副作用函数覆盖。为了解决这个问题,在 run
方法中,将当前正在执行的副作用 activeEffect
保存到新建的副作用的 parent
属性上,然后再将新建的副作用设置为当前正在执行的副作用 。在新建的副作用执行完毕后,再将存储到 parent
属性的副作用重新设置为当前正在执行的副作用。
在 ReactiveEffect
类中,还定义了一个 stop
方法,该方法用来停止并清除当前正在执行的副作用。
4、依赖收集流程
track 收集依赖
当使用代理对象访问对象的属性时,就会触发代理对象的 get
拦截函数执行,如下面的代码所示:
js
const obj = { foo: 1 }
const p = new Proxy(obj, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
}
})
p.foo
在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 get 拦截函数的执行,此时就在 get 拦截函数中调用 track 函数进行依赖收集。源码中 get 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理-访问属性的拦截》一文。
下面,我们来看看 track
函数的实现。
track 函数
js
// packages/reactivity/src/effect.ts
// 收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 如果开启了依赖收集并且有正在执行的副作用,则收集依赖
if (shouldTrack && activeEffect) {
// 在 targetMap 中获取对应的 target 的依赖集合
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map()
targetMap.set(target, (depsMap = new Map()))
}
// 从依赖集合中获取对应的 key 的依赖
let dep = depsMap.get(key)
if (!dep) {
// 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set()
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
在 track
函数中,通过一个 if
语句判断是否进行依赖收集,只有当 shouldTrack
为 true 并且存在 activeEffect
,即开启了依赖收集并且存在正在执行的副作用时,才进行依赖收集。
然后通过 target
对象从 targetMap
中尝试获取对应 target
的依赖集合 depsMap
,如果 targetMap
中不存在当前target的依赖集合,则将当前 target
添加进 targetMap
中,并将 targetMap
的 value
初始化为 new Map()
。
js
// 在 targetMap 中获取对应的 target 的依赖集合
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map()
targetMap.set(target, (depsMap = new Map()))
}
接着根据target
中被读取的 key
,从依赖集合depsMap中获取对应 key
的依赖,如果依赖不存在,则将这个 key
的依赖收集到依赖集合depsMap
中,并将依赖初始化为 new Set()
。
js
// 从依赖集合中获取对应的 key 的依赖
let dep = depsMap.get(key)
if (!dep) {
// 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set()
depsMap.set(key, (dep = createDep()))
}
最后调用 trackEffects
函数,将副作用函数收集到依赖集合depsMap
中。
js
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
trackEffects 函数
js
// 收集副作用函数
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
// 如果依赖中并不存当前的 effect 副作用函数
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
// 将当前的副作用函数收集进依赖中
dep.add(activeEffect!)
// 并在当前副作用函数的 deps 属性中记录该依赖
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
在 trackEffects
函数中,检查当前正在执行的副作用函数 activeEffect
是否已经被收集到依赖集合中,如果没有,就将当前的副作用函数收集到依赖集合中。同时在当前副作用函数的 deps
属性中记录该依赖。
5、派发更新流程
依赖收集跟派发更新核心流程大同小异,主要流程为:
readonly
->reactive
-> 对象 -> 返回reactive
(object
) -> 返回readonly
(reactive
) ->reactive
-> 原始对象获取原始值 -> 返回原始值给reactive
-> 返回原始值给readonly
,最后显示在界面
trigger 派发更新
当对属性进行赋值时,会触发代理对象的 set
拦截函数执行,如下面的代码所示:
js
const obj = { foo: 1 }
const p = new Proxy(obj, {
// 拦截设置操作
set(target, key, newVal, receiver){
// 如果属性不存在,则说明是在添加新属性,否则设置已有属性
const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newVal, receiver)
// 把副作用函数从桶里取出并执行,将 type 作为第三个参数传递给 trigger 函数
trigger(target,key,type)
return res
}
// 省略其他拦截函数
})
p.foo = 2
在上面的代码中,通过代理对象 p
访问 foo
属性,便会触发 set
拦截函数的执行,此时就在 set
拦截函数中调用 trigger 函数中派发更新。源码中 set
拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理 》一文中的「设置属性操作的拦截」小节。
下面,我们来看看 track 函数的实现。
trigger 函数
trigger 函数的源码如下:
js
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
// 该 target 从未被追踪,则不继续执行
if (!depsMap) {
// 不会追踪
return
}
// 存放所有需要派发更新的副作用函数
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 当需要清除依赖时,将当前 target 的依赖全部传入
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 处理数组的特殊情况
depsMap.forEach((dep, key) => {
// 如果对应的长度, 有依赖收集需要更新
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// 在 SET | ADD | DELETE 的情况,添加当前 key 的依赖
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// 执行可迭代的类型:ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
// 将需要执行的副作用函数收集到 effects 数组中
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
在 trigger
函数中,首先检查当前 target
是否有被追踪,如果从未被追踪过,即target
的依赖未被收集,则不需要执行派发更新,直接返回即可。
js
const depsMap = targetMap.get(target)
// 该 target 从未被追踪,则不继续执行
if (!depsMap) {
// 不会追踪
return
}
接着创建一个 Set
类型的 deps
集合,用来存储当前 target
的这个 key
所有需要执行派发更新的副作用函数。
js
// 存放所有需要派发更新的副作用函数
let deps: (Dep | undefined)[] = []
接下来就根据操作类型 type
和 key
来收集需要执行派发更新的副作用函数。
如果操作类型是 TriggerOpTypes.CLEAR
,那么表示需要清除所有依赖,将当前 target
的所有副作用函数添加到 deps
集合中。
js
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 当需要清除依赖时,将当前 target 的依赖全部传入
deps = [...depsMap.values()]
}
如果操作目标是数组,并且修改了数组的 length
属性,需要把与 length
属性相关联的副作用函数以及索引值大于或等于新的 length
值元素的相关联的副作用函数从 depsMap
中取出并添加到 deps
集合中。
js
else if (key === 'length' && isArray(target)) {
// 如果操作目标是数组,并且修改了数组的 length 属性
depsMap.forEach((dep, key) => {
// 对于索引大于或等于新的 length 值的元素,
// 需要把所有相关联的副作用函数取出并添加到 deps 中执行
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
}
如果当前的 key
不为 undefined
,则将与当前key
相关联的副作用函数添加到 deps
集合中。注意这里的判断条件 void 0
,是通过 void 运算符的形式表示 undefined
。
js
if (key !== void 0) {
deps.push(depsMap.get(key))
}
接下来通过 Switch
语句来收集操作类型为 ADD、DELETE、SET
时与 ITERATE_KEY
和 MAP_KEY_ITERATE_KEY
相关联的副作用函数。
js
// 执行可迭代的类型:ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
最后调用 triggerEffects
函数,传入收集的副作用函数,执行派发更新。
js
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
// 将需要执行的副作用函数收集到 effects 数组中
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
triggerEffects 函数
js
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
// 遍历需要执行的副作用函数集合
for (const effect of isArray(dep) ? dep : [...dep]) {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
// 如果一个副作用函数存在调度器,则调用该调度器
effect.scheduler()
} else {
// 否则直接执行副作用函数
effect.run()
}
}
}
}
在 triggerEffects
函数中,遍历需要执行的副作用函数集合,如果当前副作用函数存在调度器,则执行该调度器,否则直接执行该副作用函数的 run 方法,执行更新。
总结
本文深入分析了副作用的实现以及执行时机,并详细分析了用于存储副作用函数的targetMap
的数据结构及其实现原理。还深入分析了依赖收集track函数 以及派发更新 trigger 函数 的实现。Vue 在追踪变化时,通过 track 函数收集依赖 ,即将副作用函数 添加到 targetMap
中,通过 trigger 函数 来执行对应的副作用函来完成更新。