1. 逐行解读trigger和调度器
1.1. 副作用函数触发器Trigger
由于在讲解"优化无用依赖清理算法"时已经对track
进行了剖析,因此现在我们直接分析trigger
就好了。
export function trigger(
target: object,
// set, add, delete, clear
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 该属性没有被任何副作用函数跟踪过,所以直接返回就好了
return
}
/**
* 用于存储将要被触发的副作用函数。
* 为什么不直接通过类似depsMap.values().forEach(fn => fn())执行副作用函数呢?
* 那是因为副作用函数执行时可能会删除或增加depsMap.values()的元素,导致其中的副作用函数执行异常。
* 因此用另一个变量存储将要执行的副作用函数集合,那么执行过程中修改的是depsMap.values()的元素,而正在遍历执行的副作用函数集合结构是稳定的。
*/
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// 对象的所有属性值清空,所有依赖该响应式对象的副作用函数都将被触发
deps = [...depsMap.values()]
}
else if (key === 'length' && isArray(target)) {
// 若设置length属性,那么依赖length属性和索引值大于等于新的length属性值的元素的副作用函数都会被触发
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
}
else {
// 将依赖该属性的
if (key !== void 0) {
// 即使插入的是undefined也没有关系
deps.push(depsMap.get(key))
}
/**
* 添加间接依赖的副作用函数
* 1. 新增数组新值索引大于数组长度时,会导致数组容量被扩充,length属性也会发生变化
* 2. 新增或删除Set/WeakSet/Map/WeakMap元素时,需要触发依赖迭代器的副作用函数
* 3. 新增或删除Map/WeakMap元素时,需要触发依赖键迭代器的副作用函数
* 4. 设置Map/WeakMap元素的值时,需要触发依赖迭代器的副作用函数
*/
switch(type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
// 对于非数组,则触发通过迭代器遍历的副作用函数
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
else if (isIntegerKey(key)) {
// 对数组插入新元素,则需要触发依赖length的副作用函数
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
// 对于非数组,则触发通过迭代器遍历的副作用函数
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
// 对于Map/WeakMap需要触发依赖迭代器的副作用函数
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
}
if (deps.length === 1) {
// 过滤掉undefined
if (deps[0]) {
triggerEffects(deps[0])
}
}
else {
const effects: ReactiveEffect[] = []
// 过滤掉undefined
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
}
export function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
for (const effect of isArray(dep) ? dep : [...dep]) {
/**
* 必须保证将要触发的副作用函数(effect)不是当前运行的副作用函数(activeEffect),否则将嵌入无限递归。
* 假设存在如下情况
* let foo = reactive({ bar: 1 })
* effect(() => {
* foo.bar = foo.bar + 1
* })
* 若没有上述的保障,则将会不断递归下去直接爆栈。
*
* 假如ReactiveEffect对象的allowRecurse设置为true,那么表示不对上述问题作防御。
*/
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
// 若设置有调度器则调用调用器
effect.scheduler()
}
else {
// 立即执行副作用函数
effect.run()
}
}
}
}
1.2. 调度器Scheduler
在上一节的triggerEffects
中我们看到默认采用同步方式执行副作用函数,若要同步执行数十个副作用函数那么势必会影响当前事件循环主逻辑的执行,这时就是调度器闪亮登场的时候了。我们回顾以下petite-vue中提供的调度器吧!
import { effect as rawEffect } from '@vue/reactivity'
const effect = (fn) => {
const e: ReactiveEffectRunner = rawEffect(fn, {
scheduler: () => queueJob(e)
})
return e
}
// ./scheduler.ts
let queued = false
const queue: Function[] = []
const p = Promise.resolve()
export const nextTick = (fn: () => void) => p.then(fn)
export const queueJob = (job: Function) => {
if (!queue.includes(job)) queue.push(job)
if (!queued) {
queued = true
nextTick(flushJobs)
}
}
const flushJobs = () => {
for (const job of queue) {
job()
}
queue.length = 0
queued = false
}
副作用函数压入队列中,并将遍历队列执行其中的副作用函数后清空队列的flushJobs
压入micro queue。那么当前事件循环主逻辑执行完后,JavaScript引擎将会执行micro queue中的所有任务。