持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
关键API-observe & toJS
observe
先来看一下 observe 的用法
import { observable, observe } from '@formily/reactive'
const obs = observable({
aa: 11,
})
const dispose = observe(obs, (change) => {
console.log(change)
})
obs.aa = 22
dispose()
observe 可以主动监听一个 observable 对象并指定其监听函数。这些监听函数被保存在全局变量 ObserverListeners 中,当事件触发后,会判断触发的 observable 是否是注册监听时使用闭包保存的 target。这段核心的代码如下:
if (
node === targetNode ||
(node.targetRaw === targetRaw && node.key === operation.key)
) {
observer(new DataChange(operation, targetNode))
}
如果闭包中的 targetNode 是当前被触发的对象(Node),则会执行监听函数。
我们可以推测出,事件触发的时机其实也是在对 observable 对象进行 get 和 set 操作时,这和 Reaction 绑定和触发的时机是一致的。下面的代码可以证实我们的猜测。
const notifyObservers = (operation: IOperation) => {
ObserverListeners.forEach((fn) => fn(operation))
}
export const runReactionsFromTargetKey = (operation: IOperation) => {
let { key, type, target, oldTarget } = operation
batchStart()
// 在此处对 observers 进行通知
notifyObservers(operation)
// 代码省略
}
toJS
官方文档中对其描述是这样的:
深度递归将 observable 对象转换成普通 JS 对象
比较容易理解,这里因为源码比较长,暂时不占用篇幅了。其实质上就是在用递归进行转换。
针对数组、对象这种引用类型以 dfs 形式进行 _toJS 调用;针对原始值,则直接返回。可以试想,转换后的对象每一层子对象都不再是 observable 类型。
但我之前考虑过另一种实现 🧐,因为全局变量 ProxyRaw 的存在,如果使用 proxy 去拿到原始对象,这样是否更加简单。但是,这里就忽略了原始对象和普通 JS 对象含义是不同的。因为原始对象中可能包含 observable 类型的子对象,所以根据 toJS 实现的功能来看,只有层层遍历转换,才能达到要求。
后记
-
了解了响应式数据的内涵。
-
场景推广:可以较好地理解 Vue 实现响应式渲染的逻辑,而这种方式不仅在前端渲染框架中大量使用,其实广泛适用于一些数据驱动+自动执行的场景,比如当修改某一个配置文件后,可以实现命令的自动执行。
-
实现的关键:相较于发布订阅或观察者模式,可以使用 Proxy 这种语法层面的代理,实践元编程。当然,都有特定的绑定和触发的逻辑,reactive 就很好地展示了绑定和触发的时机。启发我们至少需要提供:
- 类似 observable 这种返回代理对象的 API
- 类似 autorun 这种自动执行的 API
-
-
理解全局变量
- 框架代码和我们平时写业务代码有很大不同。对于业务代码而言,我们有明确具据来源,但是对于框架代码而言,函数构成执行逻辑,数据来源却是未知的,是由框架使用者进行输入的。而全局变量就是连接调用者提供的数据和框架逻辑的桥梁。一般而言,全局变量会存放调用者的输入数据,在整个框架运行时有效。框架代码通过对全局变量进行操作,实际上就是在复现代码运行时的逻辑。在业务中,偏向于调用函数操作已知数据,在框架中,偏向于设计逻辑开发函数(或者对象及方法),看起来是一个上下游的关系。
- 全局变量在文件中基于前端模块化(import/export)的规范进行引用,对于引用类型,保证全局唯一性。
-
函数也是对象,可以为其添加属性
- 对 JS 语法熟悉的话,对这一点应该不难理解,reactive 向我们展示了它是如何利用这一点去简化对象结构的。
- 可以使用对象的属性建立引用关系,而引用关系适用于绑定的场景。比如在 reactive 中的 ReactionMap 的结构,存放的 reaction(函数)有属性指向其所在的 map。
-
动态绑定的实现
动态绑定指的是在执行监听函数时,会对上一次的函数解绑,重新绑定当前的监听函数,也就实现了监听函数在执行过程中可以动态改变的效果。
-
批量执行的实现
批量执行的实现可以避免多次触发,减少性能消耗。
实现的关键是提供批量执行的时机和暂存队列的结构。参考 batch 的实现,执行时机在 batchEnd 时,而整个 isBatching 的过程中都在暂存待执行函数。这里启发我们可以有类似的状态的设计。并且,可以参照 reactive 中暂存队列去重的逻辑,自行实现类似 ArraySet 的结构,那么在同一批次下的相同函数在暂存队列中最多存在一处引用。
-
依赖收集的实现
autorun.memo 和 autorun.effect 和 React 中的 Hook 有相似之处,根据依赖项是否变化决定是否执行函数(effect)或记录内部函数执行结果(memo)。内部使用了数组记录依赖项的迭代,使用 cursor 指向最新值,每次比较基于cursor 指向的值和接收的当前值。