Effect
学习 effect 源码,先从一个测试用例开始,首先我们构造了一个 counter 响应式代理,然后调用了 effect 函数传入一个会产生副作用的操作。此时 effect 中的函数会自动执行一遍,一是初始化了 dummy,二则是在 counter.num 的过程中了 get 拦截器里的 track 操作以跟踪依赖:
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})

再来回忆一下之前我们构造的响应式系统的几个概念:
effect:属性改变带来的副作用函数;dep(依赖):effect副作用函数的集合;depMap:通过属性key来映射其对应的依赖;targetMap:被代理对象target到depMap的集合;
源码中的存储结构大同小异:
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers.
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
let activeEffect: ReactiveEffect | undefined
Effect
effect 函数很简单,内部调用 createReactiveEffect 构造一个上面所说的 ReactiveEffect 函数结构,如果传入的 options 没有 lazy 标志位则立刻执行返回的函数结构。
如果传入的 fn 本身是一个 effect 结构,则解析出其 raw 中储存的原函数来构造一个新 ReactiveEffect:
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
createReactiveEffect
createReactiveEffect 返回的是一个有很多标志位(额外属性)的二加工函数体,属性说明如下:
export interface ReactiveEffect<T = any> {
(): T // 自身的函数签名;
_isEffect: true // ReactiveEffect 标志位,用于 isEffect 检测;
id: number // effect 的自增 id(唯一标识);
active: boolean // ?
raw: () => T // 传入 effect 的原函数体缓存
deps: Array<Dep> // 包含这个 effect 的 dep 指针集,一个 effect 可能同时被多个 dep 包含。
options: ReactiveEffectOptions // 选项
allowRecurse: boolean // 是否允许递归
}
export function isEffect(fn: any): fn is ReactiveEffect {
return fn && fn._isEffect === true
}
另外还要提及两个全局变量,uid 生产 effect 的自增 id 从 0 开始,activeEffect
let uid = 0
let activeEffect: ReactiveEffect | undefined
具体来看看 createReactiveEffect,先抛开 reactiveEffect 的函数体,直接看看后面为 effect 添加的属性,属性介绍在代码注释中写了。这里我们要知道这些属性是先被赋值,然后 effect 才被执行的。
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
// 先忽略
} as ReactiveEffect
effect.id = uid++ // 自增 id,从 0 开始;
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
回过头来分析 reactiveEffect 的函数体,这个函数其实还是很复杂的,因为它涉及到两个栈缓存 — effectStack 和 trackEffect:

为什么要通过 stack 的形式缓存 effect,这其实和 effect 的执行实际有关,如果 effect 不是 nested 的,那么从 effect 被创建到其被记录到某属性的 dep 的过程为下图:

流程大概是 effect 创建一个 ReactiveEffect 结构体,然后将其缓存到一个全局变量 activeEffect 上。之后执行原函数 fn,fn 中涉及 reactive get 的操作会触发拦截器,get proxy 中会将 activeEffect 记录到当前属性的 dep 上。

如果说 effect 不是嵌套的,那这个流程没有任何问题,但是一旦执行的原函数里又执行了一个 effect 函数,那么原来记录的 activeEffect 就丢失了,所以为了解决嵌套 effect 调用的问题,vue 采用 effectStack 来缓存还没有添加依赖关系的 effect。

通过 effectStack 改写的执行流程如上图,reactiveEffect中还有另一个栈 trackEffect,这个栈用于控制标志位。
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
// effect.active 这个表示最开始 true,代码走到这里说明我们调用了effect stop 函数;
// 如果没有调度者,直接返回,否则直接执行fn
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
// 如果 effectStack 里面没有这个 effect,说明它是第一次被执行;
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
}
cleanup 负责在每次 effect 执行时重新收集依赖,因为 effect 内的实际函数体 raw 可能会改变,原先依赖这个 effect 的属性 dep 可能会变化,所以这里就要先清空之前记录的 deps 指向,并且从所有原来记录的 dep 中删除这个 effect:
// 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
ReactiveEffectOptions
创建 effect 的时候可以传入一个 option 属性做配置和调试,每个属性的解释如下:
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延迟执行 effect
allowRecurse?: boolean // ? 暂时未知
scheduler?: (job: ReactiveEffect) => void // ? 暂时未知
onTrack?: (event: DebuggerEvent) => void // track 触发时的回调函数
onTrigger?: (event: DebuggerEvent) => void // trigger 触发时的回调函数
onStop?: () => void // effect 监听停止时的回调函数
}
export type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
} & DebuggerEventExtraInfo
stop
stop 函数用于清除某个 effect 副作用:
export function stop(effect: ReactiveEffect) {
if (effect.active) {
cleanup(effect)
if (effect.options.onStop) {
effect.options.onStop()
}
effect.active = false
}
}
track
track 函数本身很简单,你给他一个 target 一个 key,他帮你把当前的 activeEffect 存储到 target->key->dep,然后在给你添加一个 effect -> [dep] 的反向映射。
这里我们可以看到传递给 track 的 option 其实没啥用,单纯用来 debugger 的文字枚举:
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
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()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// using literal strings instead of numbers so that it's easier to inspect
// debugger events
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
trigger
触发依赖函数先创建一个 add 函数用来收集 active 或者 allowRecurse 的 effects,之后对应不同的 TriggerOption 和 target 类型来收集 targetMap[target]上所有属性范围的依赖:
TriggerOpTypes.CLEAR: 对应MAP等类型的clear方法,直接收集所有依赖;key === 'length' && isArray(target):改变数组长度属性,收集length属性以及idx >= length的所有索引属性的依赖。key !== void 0:非长度为0方法,即
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
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
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))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
// 依赖执行
}
依赖执行部分,先调用 effect.option 上的 onTrigger 回调做调试,然后如果 effect 有 options.scheduler 属性说明是 computed 计算 构造的响应式,调用 scheduler 代替 effect 本身:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 收集依赖
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)
}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!