本系列分三部曲：《框架实现》 《框架使用》 与 《跳出框架看哲学》，这三篇是我对数据流阶段性的总结，正好补充之前过时的文章。

本篇是 《框架实现》。

本周精读的文章是 [dob 文档](https://dobjs.github.io/dob-docs/v2/guide/introduction.html)，如果不熟悉 API，可以简单读一读，文中有些地方会提到一些函数。

## 1 引言

我觉得数据流与框架的关系，有点像网络与人的关系。

在网络诞生前，人与人之间连接点较少，大部分消息都是通过人与人之间传递，虽然信息整体性不强，但信息在局部非常完备：当你想开一家门面，找到经验丰富的经理人，可以一手包办完。

网络诞生后，如果想通过纯网络的方式，学习如何开门面，如果不是对网络很熟悉，一时半会也难以学习到全套流程。

数据流对框架来说，就像网络对人一样，总是存在着模块功能的完备性与项目整体性的博弈。

全局性强了，对整体性强要求的项目（频繁交互数据）友好，顺便利于测试，因为不利于测试的 UI 与数据关系被抽离开了。

局部性强了，对弱关联的项目友好，这样任何模块都能不依赖全局数据，自己完成所有功能。

对数据流的研究，大多集中于 “优化在某些框架的用法” “基于场景改良” “优化全局与局部数据流间关系” “函数式与面向对象之争” “对输入抽象” “数据格式转换” 这几方面。这里面参杂着统一与分离，类比到网络与人，也许最终只有人脑搬到网络中，才可以达到最终状态。

虚的就说这么多，本篇讲的是 《框架实现》，我们先钻到细节里。

## 2 精读 dob 框架实现

dob 是个类似 mobx 的框架，实现思路都很类似，如果难以读懂 mobx 的源码，可以先参考 dob 的实现原理。

### 抽丝剥茧，实现依赖追踪

> MVVM 思路中，依赖追踪是核心。
> 
> dob 中 `observe` 类似 mobx 的 `autorun`，是使用频率最高的依赖监听工具。

写作时，已经有许多文章将 vue 源码翻来覆去研究过了，因此这里就不长篇大论 MVVM 原理了。

依赖追踪分为两部分，分别是 **依赖收集** 与 **触发回调**，如果把这两个功能合起来，就是 `observe` 函数，分开的话，就是较为底层的 `Reaction`：

![](https://img.alicdn.com/imgextra/i2/O1CN01F7u1J41JTKE1ZJp8x_!!6000000001029-2-tps-534-196.png)

`Reaction` 双管齐下，一边监听用到了哪些变量，另一边在这些变量改变后，执行回调函数。`Observe` 利用 `Reaction` 实现（简化版）：

```javascript
function observe(callback) {
  const reaction = new Reaction(() => {
    reaction.track(callback)
  })

  reaction.run()
}
```

`reaction.run()` 在初始化就执行 `new Reaction` 的回调，而这个回调又恰好执行 `reaction.track(callback)`。所以 `callback` 函数中用到的变量被记录了下来，当变量更改时，会触发 `new Reaction` 的回调，又重新收集一轮依赖，同时执行了 `callback`。

这样就实现了回调函数用到的变量被改变后，重新执行这个回调函数，这就是 `observe`。

### 为什么依赖追踪只支持同步函数

> 依赖收集无法得到触发时的环境信息。

依赖收集由 getter、setter 完成，但触发时，却无法定位触发代码位于哪个函数中，所以为了依赖追踪（即变量与函数绑定），需要定义一个全局的变量标示当前执行函数，当各依赖收集函数执行没有交叉时，可以正常运作：

![](https://img.alicdn.com/imgextra/i1/O1CN01yfV1zA1bj2tudPIt6_!!6000000003500-2-tps-547-554.png)

上图右侧白色方块是函数体，`getter` 表示其中访问到某个变量的 `getter`，经由依赖收集后，变量被修改时，左侧控制器会重新调用其所在的函数。

但是，当函数嵌套函数时，就会出现异常：

![](https://img.alicdn.com/imgextra/i3/O1CN01Ex3IDC1e8M1IAvAvy_!!6000000003826-2-tps-558-312.png)

由于采用全局变量标记法，当回调函数嵌套起来时，当内层函数执行完后，实际作用域已回到了外层，但依赖收集无法获取这个堆栈改变事件，导致后续 `getter` 都会误绑定到内层函数。

异步（回调）也是同理，虽然写在一个函数体内，但执行的堆栈却不同，因此无法实现正确的依赖收集。

所以需要一些办法，将嵌套的函数放在外层函数执行完毕后，再执行：

![](https://img.alicdn.com/imgextra/i3/O1CN013SAiZf1IBAubeITp9_!!6000000000854-2-tps-219-358.png)

换成代码描述如下：

```javascript
observe(()=>{
  console.log(1)
  observe(()=>{
  	console.log(2)
  })
  console.log(3)
})
// 需要输出 1,3,2
```

当然这不是简单 setTimeout 异步控制就可以，因为依赖收集是同步的，我们要在同步基础上，实现函数执行顺序的变换。

我们可以逐层分解，**在每一层执行时，子元素如果是 `observe`，就会临时放到队列里并跳过，在父 `observe` 执行完毕后，检查并执行队列**，两层嵌套时执行逻辑如下图所示：

![](https://img.alicdn.com/imgextra/i4/O1CN01jKEwFP1ePIfuHNFkA_!!6000000003863-2-tps-421-568.png)

这些努力，就是为了保证在同步执行时，所有 `getter` 都能绑定到正确的回调函数。

### 如何结合 React

> `observe` 如何到 `render`

observe 可以类比到 React 的 render，它们都具有相同的特征：是同步函数，同时 observe 的运行机制也符合了 render 函数的需求，不是吗？

如果将 observe 用到 react render 函数，当任何 render 函数使用到的变量发生改动，对应的 render 函数就会重新执行，实现 UI 刷新。

要实现结合，用到两个小技巧：聚合生命周期、替换 render 函数，用图才能解释清楚：

![](https://img.alicdn.com/imgextra/i4/O1CN01Jv4gvd21axvSNjoDU_!!6000000007002-2-tps-655-565.png)

以上是简化版，正式版本使用 `reaction` 实现，可以更清晰的区分依赖收集与 rerender 阶段。

### 如何避免在 view 中随意修改变量

为了使用起来具有更好的可维护性，需要限制依赖追踪的功能，使值不能再随意的修改。可见，强大的功能，不代表在数据流场景的高可用性，恰当的约束反而会更好。

因此引入 `Action` 概念，在 `Action` 中执行的变量修改，不仅会将多次修改聚合成一次 render，而且不在 `Action` 中的变量修改会抛出异常。

`Action` 类似进栈出栈，当栈深度不为 0 时，进行的任何的变量修改，拦截到后就可以抛出异常了。

### 有层次的实现 Debug

> 一层一层功能逐渐冒泡。

调试功能，在依赖追踪、与 react 结合这一层都需要做，怎样分工配合才能保证功能不冗余，且具有良好的拓展性呢？

数据流框架的 Debug 分为数据层和 UI 层，顺序是 **dob 核心记录 debug 信息 -> dob-devtools 读取再加工，强化 UI 信息**。

在 UI 层不止可以简单的将对象友好展示出来，更可以通过额外信息采集，将 Action 与 UI 元素绑定，让用户找到任意一次 Action 触发时，rerender 了哪些 UI 元素，以及每个 UI 元素分别与哪些 Action 绑定。 

由于数据流需要一个 `Provider` 提供数据源，与 `Connect` 注入数据，所以可以将所有与数据流绑定的 UI 元素一一映射到 Debug UI，就像一面镜子一样映射：

![](https://img.alicdn.com/imgextra/i2/O1CN01lrstnp1oyhRl6G5SY_!!6000000005294-2-tps-1211-512.png)

通过 Debug UI，将 debug 信息与 UI 一一对应，实现 [dob-react-devtools](https://github.com/dobjs/dob-react-devtools) 的效果。

### Debug 功能如何解耦

> 解耦还能方便许多功能拓展，比如支持 redux。

我得答案是事件。通过精心定义的一系列事件，制造出一个具有生命周期的工具库！

在所有 `getter` `setter` 节点抛出相关信息，Debug 端订阅这些事件，找到对自己有用的，记录下来。例如：

```javascript
event.on("get", info => {
  // 不在调试模式
  if (!globalState.useDebug) {
    return
  }

  // 记录调用堆栈..
})
```

Dob 目前支持这几种事件钩子：

- get: 任何数据发生了 getter。
- set: 任何数据发生了 setter。
- deleteProperty: 任何数据的 key 被移除时。
- runInAction: 调用了 Action。
- startBatch: 任意 Action 入栈。
- endBatch: 任意 Action 出栈。

并且在关键生命周期节点，还要遵守调用顺序，比如以下是 `Action` 触发后，到触发 observe 的顺序：

`startBatch` -> `debugInAction` -> `...multiple nested startBatch and endBatch` -> `debugOutAction` -> `reaction` -> `observe`

如果未开启 debug，执行顺序简化为：

`startBatch` -> `...multiple nested startBatch and endBatch` -> `reaction` -> `observe`

订阅了这些事件，可以完成类似 redux-dev-tools 的功能。

## 3 总结

由于篇幅有限，本文介绍的《框架实现》均是一些上层设计，很少有代码讲解。因为我觉得一篇引发思考的文章不应该贴太多的代码，况且人脑处理图形的效率远远高于文字、略高于代码，所以通过一些图来展示。如果想看代码实现，可以读 [dob 源码](https://github.com/dobjs/dob)。

如希望详细了解依赖注入实现流程，请看 [从零开始用 proxy 实现 mobx](https://github.com/ascoders/blog/issues/19)。

下一篇是 《框架使用》，会站在使用者的角度思考数据流。当然不是下一篇精读，因为要换换胃口，也给我一些缓冲时间去整理。

### 更多讨论

> 讨论地址是：[精读《dob - 框架实现》 · Issue #48 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/48)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，每周五发布。**
