持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情
主流程02- Reaction 的绑定和触发
在前文中,我们聚焦到了 baseHandler, 在其 get 和 set 方法中,我们发现了执行绑定和触发监听的两个重要函数,他们是 bindTargetKeyWithCurrentReaction 和 runReactionsFromTargetKey。
在上面的描述中,我大多数时间使用监听函数来代指 Reaction,实际上,Reaction 不仅有触发 Tracker 的功能,还包括了任务调度的能力。我们可以将 Reaction 理解为 Tracker Manager 一类的角色,负责在合适的时机执行 Tracker。并且,需要明确的是和 observable 对象属性建立绑定关系的就是 Reaction。
绑定 Reaction
首先来看 bindTargetKeyWithCurrentReaction
// @formily/reactive/src/reaction
export const bindTargetKeyWithCurrentReaction = (operation: IOperation) => {
let { key, type, target } = operation
if (type === 'iterate') {
key = ITERATION_KEY
}
const current = ReactionStack[ReactionStack.length - 1]
if (isUntracking()) return
if (current) {
DependencyCollected.value = true
addReactionsMapToReaction(current, addRawReactionsMap(target, key, current))
}
}
这段代码中最关键的部分就是:
- 从一个名为
ReactionStack的变量中获取栈顶的 Reaction,将其命名为 current。 - 将 current 作为参数传递给函数
addRawReactionsMap和addReactionsMapToReaction执行绑定的逻辑。
要理解这段代码,首先我们要认识一下上述提到的 ReactionStack 和两个绑定相关的函数。
ReactionStack
从变量名上理解,这是一个存储 Reaction 的栈,并且根据代码上下文可以确定这是一个全局变量(在 ./environment 文件中有其定义)。既然这里是取出栈顶元素,那么我们需要追溯一下是在何时何地进行的 Reaction 入栈操作。
Easy,在 ./autorun 中,我们可以看到这样一段代码 🧐。
// @formily/reactive/src/autorun
export const autorun = (tracker: Reaction, name = 'AutoRun') => {
const reaction: Reaction = () => {
if (!isFn(tracker)) return
if (reaction._boundary > 0) return
if (ReactionStack.indexOf(reaction) === -1) {
releaseBindingReactions(reaction)
try {
batchStart()
ReactionStack.push(reaction)
tracker()
} finally {
ReactionStack.pop()
reaction._boundary++
batchEnd()
reaction._boundary = 0
reaction._memos.cursor = 0
reaction._effects.cursor = 0
}
}
// 代码省略...
reaction()
// 代码省略...
}
非常奇妙,在这里我们不仅找到了 ReactionStack 的入栈操作,并且还发现在 autorun 内部,有 Reaction 的定义和实现,即代码中的 reaction 函数。
Q: ReactionStack 是何时进行入栈的呢?
A: 正是在执行 reaction 时,并且,入栈的正好就是正在执行的这个 reaction 函数本身。
回忆一下,我们知道在调用 autorun 时,传入的第一个参数是一个 tracker 函数(虽然在这里 tracker 函数的类型也是 Reaction, 不过暂时不影响目前的理解),一般 tracker 内部会对 observable 对象进行属性访问(读操作),即会触发对象代理的 get。
所以,关系到 ReactionStack 的执行顺序是这样的:
-
ReactionStack 压入 reaction
-
执行 tracker
-
在 tracker 中触发 handler.get
-
在 get 中调用 bindTargetKeyWithCurrentReaction
- 获取栈顶的 reaction,执行绑定
-
-
-
ReactionStack 出栈 reaction
由此,我们可以认识到,
- ReactionStack 存在的意义就是暂时存储正在执行的 Reaction,便于将该 Reaction 传递给绑定函数。
- 当 Reaction 执行时,会调用内部的 tracker。tracker 在执行时会绑定调用它的 Reaction。
addRawReactionsMap vs addReactionsMapToReaction
这里就不摘取代码了,我们直接看下方的图理解绑定的逻辑。
reactionMap
假设有待绑定的 reaction1 和目标 observable 对象 obs1、待绑定的属性 prop1。
addRawReationsMap是将 obs1.prop1 映射 reaction1 并加入到 reactionsMap1 中。reactionsMap1 相当于 obs1 的所有绑定关系的集合,他和 obs1 建立映射。而全局的RawReactionsMap则是所有原对象(rawObject)映射到其绑定关系的 Map 的集合。这里由于有嵌套的 Map,所以在加入待绑定的 reaction 时,先加入到内层的 Map 。addReactionsMapToReaction是创建反向的映射关系集合。对于上述的 reaction1 而言,将其所在的 reactionsMap1 和 reactionsMap2 分别加入到其“私有属性”_reactionsSet中。
因此,已知 observable 对象及属性名,通过全局的 RawReactionsMap 和每个 reaction 上的 _reactionsSet ,可以找到对应的 reaction;已知 reaction ,可以找到其绑定的对象。利用 Map 这种结构,很容易建立双向绑定关系。
触发 Reaction
Reaction 触发的逻辑包含在 runReactionsFromTargetKey 中,首先看一下这部分代码。
// @formily/reactive/src/reaction
export const runReactionsFromTargetKey = (operation: IOperation) => {
let { key, type, target, oldTarget } = operation
batchStart()
notifyObservers(operation)
if (type === 'clear') {
oldTarget.forEach((_: any, key: PropertyKey) => {
runReactions(target, key)
})
} else {
runReactions(target, key)
}
if (type === 'add' || type === 'delete' || type === 'clear') {
const newKey = Array.isArray(target) ? 'length' : ITERATION_KEY
runReactions(target, newKey)
}
batchEnd()
}
从上面的代码中,我们注意到根据 operation.type 会进行条件语句的执行,这里的操作类型中包含了
clear、add、delete 以及没有在上面代码出现的 set。
补充下,当我们在进行对象属性的“写操作”时, 可能会触发 handler.set、handler.delete 等,而触发 handler.set 时,对应这里的操作类型其实有两种,即 add 和 set(这也是由于 JS 语言中对象属性可以被动态添加的特点)。
如果操作类型是 set,即表示对对象原有的属性进行修改,在 runReactionsFromTargetKey 执行时,会调用 runReactions 并传递原来的 key 值,其他的操作类型也有类似的调用。
那么接下来就该看看 runReactions 做了什么事情啦。
runReactions
runReactions的具体代码如下所示:
// @formily/reactive/src/reaction
const runReactions = (target: any, key: PropertyKey) => {
const reactions = getReactionsFromTargetKey(target, key)
const prevUntrackCount = UntrackCount.value
UntrackCount.value = 0
for (let i = 0, len = reactions.length; i < len; i++) {
const reaction = reactions[i]
if (reaction._isComputed) {
reaction._scheduler(reaction)
} else if (isScopeBatching()) {
PendingScopeReactions.add(reaction)
} else if (isBatching()) {
PendingReactions.add(reaction)
} else {
if (isFn(reaction._scheduler)) {
reaction._scheduler(reaction)
} else {
reaction()
}
}
}
UntrackCount.value = prevUntrackCount
}
这段代码的主要流程是:
- 调用 getReactionsFromTargetKey 获取已知对象和其 key 所绑定的 reactions(实际就是通过全局的 RawReactionsMap 获取的)。
- 遍历 target.key 上的所有绑定的 reactions,根据当前的执行状态决定 reaction 是否执行或者暂存在 PendingScopeReactions/PendingReactions 中。
遍历 reaction 时,重要的条件判断语句是 isScopeBatching 和 isBatching,如果处于 ScopeBatching 状态或者 Batching 状态,则会将 reaction 暂存在一个待处理的队列中,稍后执行。由于这两个状态的区别在于是否是局部作用域的,有一定的相似性,所以稍后在进行主流程分析时,我们暂时只关注 Batching 这个状态。
当然,如果不满足上述的状态,则立即执行 reaction。而 reaction 的执行有两种方式。
- 其一是调用 reaction._scheduler,这种执行方式适用于使用 reactive 提供的 reaction API 创建的 Reaction(后续还会提到这个 _scheduler)
- 其二是直接调用 reaction,这种方式适用于 autorun 创建的 Reaction。
然后,我们关注一下 PendingReactions 中待执行的 reactions 是什么时候被执行的 🧐。
Easy,在名为 batchEnd 函数中我们发现这样的调用:
executePendingReactions()
这个执行函数应该就是遍历 PendingReactions 并执行吧~
export const executePendingReactions = () => {
PendingReactions.forEachDelete((reaction) => {
if (isFn(reaction._scheduler)) {
reaction._scheduler(reaction)
} else {
reaction()
}
})
}
果然,我们发现了执行 reaction 的逻辑 🤩。而还记得 batchEnd 在哪里调用的吗?回到一开始的 runReactionsFromTargetKey ,当 runReactions 执行完成后,不就调用了batchEnd。
根据上面的梳理,我们发现不论在绑定 Reaction 还是触发 Reaction,都包含了 batchStart、batchEnd 以及 isBatching 等逻辑,看来很有必要了解这个 batch 到底有什么样的作用。但在那之前,让我们再将 Reaction 绑定和触发的流程连贯地串一下吧。
Reaction 的运行机制
我在这里使用了一张大图,以最基本的 autorun 为例,包含了其绑定和触发的流程。
autorun
在上图中,我们可以发现 Reaction 在代码中的两处调用,首先是在 autorun 首次执行时,继而是在后续触发 handler.set 时。Reaction 每次执行都会清除原有绑定关系,开启 batch,并调用内部的 tracker。tracker 执行时,会对 Reaction 进行再度绑定。从而形成一个类似封闭状态机的循环模式。