# effect 方法剖析与实现
# 完整代码
export type EffectFn = { | |
(): any; | |
deps: Array<Set<EffectFn>>; | |
options?: Options; | |
}; | |
type Options = { | |
scheduler?: (fn: EffectFn) => void; | |
lazy?: boolean; | |
}; | |
let activeEffect: EffectFn; | |
const effectStarck: Array<EffectFn> = []; | |
const targetMap: WeakMap< | |
object, | |
Map<any, Set<EffectFn> | undefined> | undefined | |
> = new WeakMap(); | |
export function effect(fn: () => any, options?: Options) { | |
const effectFn: EffectFn = () => { | |
clearUp(effectFn); | |
activeEffect = effectFn; | |
effectStarck.push(effectFn); | |
const res = fn(); | |
effectStarck.pop(); | |
activeEffect = effectStarck[effectStarck.length - 1]; | |
return res; | |
}; | |
effectFn.deps = [] as Array<Set<EffectFn>>; | |
effectFn.options = options; | |
if (effectFn.options?.lazy) { | |
return effectFn; | |
} | |
effectFn(); | |
} | |
function clearUp(fn: EffectFn) { | |
fn.deps?.forEach((deps) => { | |
deps.delete(fn); | |
}); | |
} |
# effect 概述
effect
是一个用来注册副作用的函数,它默认会将注册的副作用立即执行一次,并支持传递一个选项参数 options
,可以配置是否立即执行,将副作用函数返回以便于手动执行 and 配置调度器 scheduler
# 部分变量,属性,方法介绍
activeEffect
: 用来存储当前的副作用函数effectStrack
: 因为effect
可能会嵌套effect
,所以需要用effectStrack
来存储这些嵌套的副作用函数,当内层的effect
执行完毕后,将内层的副作用函数弹出,将外层的副作用函数赋值给用来存储当前的副作用函数的activeEffect
。targetMap
: 一种WeakMap
结构,用来存储target
,key
, 副作用之间的对应关系。effectFn.deps
: 类型为Array<Set>
,用来存储保存的有该副作用的target
对应的key
对应的Set
, 以便于在副作用函数再次执行的时候调用的clearUp
方法来将effectFn
从这些Set
中移除。clearUp
: 利用effectFn.deps
将effectFn
从其所在的Set
中移除。
# 详细步骤
- 定义
effectFn
增强版副作用函数,以便于增强它,并做进一步处理effectFn
函数将做的事情- 调用
clearUp
来将effectFn
从其所在的Set
中移除,从而避免trigger
触发没必要的副作用函数。 - 将
effectFn
赋值给activeEffect
, 并将effectFn
放进effectStack
。 - 调用
fn
副作用函数,将返回值保存进res
。 - 将
effectFn
从activeEffect
中弹出,并将activeEffect
的最后一个元素赋值给activeEffect
(让activeEffect
指向外层的副作用函数) - 返回
res
- 调用
- 在
effectFn
函数上添加属性,增强它- 在
effectFn
上添加deps
属性,用来存储保存有该副作用函数的target
对应的key
的对应的Set
, 以提供给clearUp
函数使用。 - 在
effectFn
函数上添加options
属性,保存副作用函数的配置选项。
- 在
- 根据
effectFn
函数的配置选项options
属性,来进行下一步的操作- 如果
effectFn.options.lazy
为true
,则不立即执行effectFn
, 而是将effectFn
返回,手动调用。 - 如果为
false
, 则立即执行。
- 如果
# track 方法剖析与实现
# 完整代码
export function track(target: {}, key: any) { | |
if (!activeEffect) return; | |
let depsMap = targetMap.get(target); | |
if (!depsMap) targetMap.set(target, (depsMap = new Map())); | |
let deps = depsMap.get(key); | |
if (!deps) depsMap.set(key, (deps = new Set())); | |
deps.add(activeEffect); | |
activeEffect.deps.push(deps); | |
} |
# track 概述
用来收集副作用,将副作用存储进对应的 target
对应的 key
对应的 Set
。
# 详细步骤
- 边界处理
- 如果
activeEffect == undefined
,则直接返回
- 如果
- 将副作用函数存储进对应的
Set
- 调用
targetMap.get(target)
取出target
对应的Map
(里面存储的是key
和对应的Set
)并赋值给depsMap
。如果depsMao
为空,则调用targetMap.set(target, (depsMap = new Map))
新建一个Map
存进targetMap
。 - 调用
depsMap.get(key)
取出key
对应的Set
(里面存储的是key
对应的副作用函数) 并赋值给deps
。如果deps
不存在,则调用depsMap.set(key, (deps = new Set()))
新建一个Set
存进depsMap
。 - 调用
deps.add(activeEffect)
将副作用函数存储进Set
.
- 调用
- 将存有该副作用函数的
Set
存储进activeEffect.deps
# trigger 方法剖析与实现
# 完整代码
export function trigger(target: {}, key: any) { | |
const depsMap = targetMap.get(target); | |
if (!depsMap) return; | |
const effects = depsMap.get(key); | |
if (!effects) return; | |
const effectsToRun: Set<EffectFn> = new Set(); | |
effects?.forEach((effectFn) => { | |
if (effectFn !== activeEffect) { | |
effectsToRun.add(effectFn); | |
} | |
}); | |
effectsToRun.forEach((effectFnToRun) => { | |
if (effectFnToRun.options?.scheduler) { | |
effectFnToRun.options.scheduler(effectFnToRun); | |
} else { | |
effectFnToRun(); | |
} | |
}); | |
} |
# trigger 概述
用来调用 track
收集的副作用,如果有调度器,则调用调度器。
# 详细步骤
- 取出对应的
target
的对应的key
的对应的Set
, 并做边界处理- 调用
targetMap.get(target)
取出target
对应的Map
赋值给depsMap
。如果depsMap
不存在,则直接返回 - 调用
depsMap.get(key)
取出key
对应的Set
并复制给effects
,如果effects
不存在,则直接返回。
- 调用
- 定义一个
effectsToRun
, 将effects
存储的副作用函数全部添加到effectsToRun
, 以等待遍历effectsToRun
执行存储的所有副作用函数。(之所以这么做是因为,副作用函数在执行的时候会先调用clearUp
将其从Set
中删除,执行fn
的时候又会将effectFn
添加到Set
, 这样就会陷入无限循环, 所以定义一个新的Set
来避免这个问题) - 调用
effectsToRun.forEach
来遍历它,并调用存储的副作用函数,但是在调用前会做判断,如果effectToRun.options.scheduler
存在的话,会调用effectToRun.options.sheduler
而不是调用effectToRun