@formily/reactive 源码解读(7)

785 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

关键API-observe & toJS

observe

先来看一下 observe 的用法

import { observable, observe } from '@formily/reactive'

const obs = observable({
  aa11,
})

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 实现的功能来看,只有层层遍历转换,才能达到要求。

后记

  1. 了解了响应式数据的内涵。

    1. 场景推广:可以较好地理解 Vue 实现响应式渲染的逻辑,而这种方式不仅在前端渲染框架中大量使用,其实广泛适用于一些数据驱动+自动执行的场景,比如当修改某一个配置文件后,可以实现命令的自动执行。

    2. 实现的关键:相较于发布订阅或观察者模式,可以使用 Proxy 这种语法层面的代理,实践元编程。当然,都有特定的绑定和触发的逻辑,reactive 就很好地展示了绑定和触发的时机。启发我们至少需要提供:

      1. 类似 observable 这种返回代理对象的 API
      2. 类似 autorun 这种自动执行的 API
  2. 理解全局变量

    1. 框架代码和我们平时写业务代码有很大不同。对于业务代码而言,我们有明确具据来源,但是对于框架代码而言,函数构成执行逻辑,数据来源却是未知的,是由框架使用者进行输入的。而全局变量就是连接调用者提供的数据和框架逻辑的桥梁。一般而言,全局变量会存放调用者的输入数据,在整个框架运行时有效。框架代码通过对全局变量进行操作,实际上就是在复现代码运行时的逻辑。在业务中,偏向于调用函数操作已知数据,在框架中,偏向于设计逻辑开发函数(或者对象及方法),看起来是一个上下游的关系。
    2. 全局变量在文件中基于前端模块化(import/export)的规范进行引用,对于引用类型,保证全局唯一性。
  3. 函数也是对象,可以为其添加属性

    1. 对 JS 语法熟悉的话,对这一点应该不难理解,reactive 向我们展示了它是如何利用这一点去简化对象结构的。
    2. 可以使用对象的属性建立引用关系,而引用关系适用于绑定的场景。比如在 reactive 中的 ReactionMap 的结构,存放的 reaction(函数)有属性指向其所在的 map。
  4. 动态绑定的实现

    动态绑定指的是在执行监听函数时,会对上一次的函数解绑,重新绑定当前的监听函数,也就实现了监听函数在执行过程中可以动态改变的效果。

  5. 批量执行的实现

    批量执行的实现可以避免多次触发,减少性能消耗。

    实现的关键是提供批量执行的时机和暂存队列的结构。参考 batch 的实现,执行时机在 batchEnd 时,而整个 isBatching 的过程中都在暂存待执行函数。这里启发我们可以有类似的状态的设计。并且,可以参照 reactive 中暂存队列去重的逻辑,自行实现类似 ArraySet 的结构,那么在同一批次下的相同函数在暂存队列中最多存在一处引用。

  6. 依赖收集的实现

    autorun.memo 和 autorun.effect 和 React 中的 Hook 有相似之处,根据依赖项是否变化决定是否执行函数(effect)或记录内部函数执行结果(memo)。内部使用了数组记录依赖项的迭代,使用 cursor 指向最新值,每次比较基于cursor 指向的值和接收的当前值。