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介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!