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

本篇是收官之作 《前端数据流哲学》。

## 1 引言

写这篇文章时，很有压力，如有不妥之处，欢迎指正。

同时，由于这是一篇**佛系文章**，所以不会得出你应该用 某某 框架的结论，你应该当作消遣来阅读。

## 2 精读

首先数据流管理模式，比较热门的分为三种。

- 函数式、不可变、模式化。典型实现：Redux - 简直是正义的化身。
- 响应式、依赖追踪。典型实现：Mobx。
- 响应式，和楼上区别是以流的形式实现。典型实现：Rxjs、xstream。

当然还有第四种模式，裸奔，其实有时候也挺健康的。

数据流使用通用的准则是：副作用隔离、全局与局部状态的合理划分，以上三种数据流管理模式都可以实现，唯有是否强制的区别。

### 2.1 从时间顺序说起

一直在思考如何将这三个思维串起来，后来想通了，按照时间顺序串起来就非常自然。

暂时略过 Prototype、jquery 时代，为什么略过呢？因为当时前端还在野蛮人时代，生存问题都没有解决，哪还有功夫思考什么数据流，设计模式？前端也是那时候被觉得比后端水的。

好在前端发展越来越健康，大坑小坑被不断填上，加上硬件性能的提高，同时需求又越来越复杂，是时候想想该如何组织代码了。

最先映入眼帘的是 angular，搬来的 mvvm 思想真是为前端开辟了新的世界，发现代码还可以这么写！虽然 angluar 用起来很重，但 mvvm 带来的数据驱动思想已经越来越深入人心，随后 react 就突然火起来了。

> 其实在 react 火起来之前，有一个框架一步到位，进入了 react + mobx 时代，对，就是 [avalon](https://github.com/RubyLouvre/avalon)。avalon 也非常火，但是一个框架要成功，必须天时、地利、人和，当时时机不对，大家处于 angular 疲惫期，大多投入了 react 的怀抱。

可能有些主观，但我觉得 react 能火起来，主要因为大家认为它就是轻量 angular + 继承了数据驱动思想啊，非常符合时代背景，同时一大波概念被炒得火热，状态驱动、单向数据流等等，基本上用过 angular 的人都跟上了这波节奏。

虽然 react 内置了分形数据流管理体系，但总是强调自己只是 View 层，于是数据层增强的框架不断涌现，从 flux、reflux、到 redux。不得不说，react 真的推动了数据流管理的独立，让我们重新认识了数据流管理的重要性。

redux 概念太超前了，一步到位强制把副作用隔离掉了，但自己又没有深入解决带来的代码冗余问题，让我们又爱又恨，于是一部分人把目光转向了 mobx，这个响应式数据流框架，这个没有强制分离副作用，所以写起来很舒服的框架。

当然 mobx 如果仅仅是 mvvm 就不会火起来了，毕竟 angular 摆在那。主要是乘上了 react 这趟车，又有很多质疑 angular 脏检测效率的声音，mobx 也火了起来。当然，作为前端的使命是优化人机交互，所以我们都知道，用户习惯是最难改变的，直到现在，redux 依然是绝对主流。

mobx 还在小范围推广时，另一个更偏门的领域正刚处于萌芽期，就是 rxjs 为代表的框架，和 mobx 公用一个 observable 名词，大家 mobx 都没搞清楚，更是很少人会去了解 rxjs。

> 当 mobx 逐渐展露头角时，笔者做了一个类似的库：[dob](https://github.com/dobjs/dob)。主要动机是 mobx 手感还不够完美，对于新赋值变量需要用一些 extendObservable 等 api 修饰，正好发现浏览器对 proxy 支持已经成熟，因此笔者后来几乎所有个人项目几乎都用 dob 替代了 mobx。

这一时期三巨头之一的 vue 火了起来，成功利用：如果 ”react + mobx 很好用，那为什么不用 vue？“ 的 flag 打动了我。

一直到现在，前端已经发展到可谓五花八门的地步，typescript 打败 flow 几乎成为了新的 js，出现了 ember、clojurescript 之后，各大语言也纷纷出了[到 js 的编译实现](http://www.open-open.com/lib/view/open1430293143865.html)，陆陆续续的支持编译到 webassembly，react 作者都弃坑 js 创造了新语言 reason。

> 之前写过一篇[初步认识 reason 的精读](https://zhuanlan.zhihu.com/p/32928927)。

能接下来这一套精神洗礼的前端们，已经养出内心波澜不惊的功夫，小众已经不会成为跨越舒适区的门槛，再学个 rxjs 算啥呢？（开个玩笑，rxjs 社区不乏深耕多年的巨匠）所以最近 rxjs 又被炒的火热。

所以，从时间顺序来看，我们可以从 redux - mobx - rxjs 的顺序解读这三个框架。

### 2.2 redux 带来了什么

redux 是强制使用全局 store 的框架，尽管无数人在尝试将其做到局部化。

当然，一方面是由于时代责任，那时需要一个全局状态管理工具，弥补 react 局部数据流的不足。最重要的原因，是 redux 拥有一套几乎洁癖般完美的定位，就是要**清晰**、**可回溯**。

几乎一切都是为了这两个词准备的。第一步就要从**分离副作用**下手，因为副作用是阻碍代码清晰、以及无法回溯的第一道障碍，所以 action + reducer 概念闪亮登场，完美解决了副作用问题。可能是参考了 koa 中间件的设计思路，redux middleware 将 action 对接到 reducer 的黑盒的控制权暴露给了开发者。

由 redux middleware 源码阅读引发的函数式热，可能又拉近了开发者对 rxjs 的好感。同时高阶函数概念也在中间件源码中体现，几乎是为 react 高阶组件做铺垫。

社区出现了很多方案对 redux 异步做支持，从 redux-thunk 到 redux-saga，redux 带来的异步隔离思想也逐渐深入人心。同时基于此的一套高阶封装框架也层出不穷，建议用一个就好，比如 [dva](https://github.com/dvajs/dva)。

第二步就是**解决阻碍回溯的“对象引用”机制**，将 immutable 这套庞大思想搬到了前端。这下所有状态都不会被修改，基于此的 redux-dev-tools “时光机” 功能让人印象深刻。

> Immutable 具体实现可以参考笔者之前写的一篇精读：[精读 Immutable 结构共享](https://zhuanlan.zhihu.com/p/27133830)。

当然，由于很像事件机制的 `dispatch` 导致了 redux 对 ts 支持比较繁琐，所以对 redux 的项目，维护的时候需要频繁使用全文搜索，以及至少在两个文件间来回跳跃。

### 2.3 mobx 带来了什么

mobx 是一个非常灵活的 TFRP 框架，是 FRP 的一个分支，将 FRP 做到了透明化，也可以说是自动化。

> 从函数式（FP），到 FRP，再到 TFRP，之间只是拓展关系，并不意味着单词越长越好。

之前说过了，由于大家对 redux 的疲劳，让 mobx 得以迅速壮大，不过现在要从另一个角度分析。

mobx 带来的概念从某种角度看，与 rxjs 很像，比如，都说自己的 observable 有多神奇。那么 observable 到底是啥呢？

可以把 observable 理解为信号源，每当信号变化时，函数流会自动执行，并输出结果，对前端而言，最终会使视图刷新。这就是数据驱动视图。然而 mobx 是 TFRP 框架，每当变量变化时，都会自动触发数据源的 dispatch，而且各视图也是自动订阅各数据源的，我们称为依赖追踪，或者叫自动依赖绑定。

笔者到现在还是认为，TFRP 是最高效的开发方式，自动订阅 + 自动发布，没什么比这个更高效了。

但是这种模式有一个隐患，它引发了副作用对纯函数的污染，就像 redux 把 action 与 reducer 合起来了一样。同时，对 props 的直接修改，也会导致与 react 对 props 的不可变定义冲突。因此 mobx 后来给出了 action 解决方案，解决了与 react props 的冲突，但是没有解决副作用未强制分离的问题。

笔者认为，副作用与 mutable 是两件事，关于 mutable 与副作用的关系，后文会有说明。也就是 mobx 没有解决副作用问题，不代表 TFRP 无法分离副作用，而且 mutable 也不一定与 **可回溯** 冲突，比如 mobx-state-tree，就通过 mutable 的方式，完成了与 redux 的对接。

前端对数据流的探索还在继续，mobx 先提供了一套独有机制，后又与 redux 找到结合点，前端探索的脚步从未停止。

### 2.4 rxjs 带来了什么

rxjs 是 FRP 的另一个分支，是基于 Event Stream 的，所以从对 view 的辅助作用来说，相比 mobx，显得不是那么智能，但是对数据源的定义，和 TFRP 有着本质的区别，似的 rxjs 这类框架几乎可以将任何事件转成数据源。

同时，rxjs 其对数据流处理能力非常强大，当我们把前端的一切都转为数据源后，剩下的一切都由无所不能的 rxjs 做数据转换，你会发现，副作用已经在数据源转换这一层完全隔离了，接下来会进入一个美妙的纯函数世界，最后输出到 dom driver 渲染，如果再加上虚拟 dom 的点缀，那岂不是。。岂不就是 [cyclejs](https://github.com/cyclejs/cyclejs) 吗？

> 多提一句，rxjs 对数据流纯函数的抽象能力非常强大，因此前端主要工作在于抽一个工具，将诸如事件、请求、推送等等副作用都转化为数据源。cyclejs 就是这样一个框架：提供了一套上述的工具库，与 dom 对接增加了虚拟 dom 能力。

rxjs 给前端数据流管理方案带来了全新的视角，它的概念由 mobx 引发，但解题思路却与 redux 相似。

rxjs 带来了两种新的开发方式，第一种是类似 cyclejs，将一切前端副作用转化为数据源，直接对接到 dom。另一种是类似 [redux-observable](https://github.com/redux-observable/redux-observable)，将 rxjs 数据流处理能力融合到已有数据流框架中，

redux-observable 将 action 与 reducer 改造为 stream 模式，对 action 中副作用行为，比如发请求，也提供了封装好的函数转化为数据源，因此，将 redux middleware 中的副作用，转移到了数据源转换做成中，让 action 保持纯函数，同时增强了原本就是纯函数的 reducer 的数据处理能力，非常棒。

如果说 redux-saga 解决了异步，那么 redux-observable 就是解决了副作用，同时赠送了 rxjs 数据处理能力。

回头看一下 mobx，发现 rxjs 与 mobx 都有对 redux 的增强方案，前端数据流的发展就是在不断交融。

我们不但在时间线上，将 redux、mobx、rxjs 串了起来，还发现了他们内在的关联，这三个思想像一张网，复杂的交织在一起。

### 2.5 可以串起来些什么了

我们发现，redux 和 rxjs 完全隔离了副作用，是因为他们有一个共性，**那就是对前端副作用的抽象**。

redux 通过在 action 做副作用，将副作用隔离在 reducer 之外，使 reducer 成为了纯函数。

rxjs 将副作用先转化为数据源，将副作用隔离在管道流处理之外。

**唯独 mobx，缺少了对副作用抽象这一层**，所以导致了代码写的比 redux 和 rxjs 更爽，但副作用与纯函数混杂在一起，因此与函数式无缘。

有人会说，mobx 直接 mutable 改变对象也是导致副作用的原因，笔者认为是，也不是，看如下代码：

```typescript
obj.a = 1
```

这段代码在 js 中铁定是 mutable 的？不一定，同样在 c++ 这些可以重载运算符的语言中也不一定了，`setter` 语法不一定会修改原有对象，比如可以通过 `Object.defineProperty` 来重写 `obj` 对象的 `setter` 事件。

由此我们可以开一个脑洞，通过运算符重载，让 mutable 方式得到 immutable 的结果。在笔者博客 [Redux 使用可变数据结构](https://github.com/ascoders/blog/issues/21) 有说明原理和用法，而且 mobx 作者 [mweststrate](https://github.com/mweststrate) 是这么反驳那些吐槽 mobx 缺少 redux 历史回溯能力的声音的：

```javascript
autorun(() => {
  snapshots.push(Object.assign({}, obj))
})
```

思路很简单，在对象有改动时，保存一张快照，虽然性能可能有问题。这种简单的想法开了个好头，其实只要在框架层稍作改造，便可以实现 mutable 到 immutable 的转换。

比如 mobx 作者的新作：[immer](https://github.com/mweststrate/immer) 通过 proxy 元编程能力，将 `setter` 重写为 `Object.assign()` 实现 mutable 到 immutable 的转换。

笔者的 [dob-redux](https://github.com/dobjs/dob-redux) 也通过 proxy，调用 `Immutablejs.set()` 实现 mutable 到 immutable 的转换。

### 组件需要数据流吗

真的是太看场景了。首先，业务场景的组件适合绑定全局数据流，业务无关的通用组件不适合绑定全局数据流。同时，对于复杂的通用组件，为了更好的内部通信，可以绑定支持分形的数据流。

然而，如果数据流指的是 rxjs 对数据处理的过程，那么任何需要数据复杂处理的场合，都适合使用 rxjs 进行数据计算。同时，如果数据流指的是对副作用的归类，那任何副作用都可以利用 rxjs 转成一个数据源归一化。当然也可以把副作用封装成事件，或者 promise。

对于副作用归一化，笔者认为更适合使用 rxjs 来做，首先事件机制与 rxjs 很像，另外 promise 只能返回一次，而且之后 `resolve` `reject` 两种状态，而 Observable 可以返回多次，而且没有内置的状态，所以可以更加灵活的表示状态。

所以对于各类业务场景，可以先从人力、项目重要程度、后续维护成本等外部条件考虑，再根据具体组件在项目中使用场景，比如是否与业务绑定来确定是否使用，以及怎么使用数据流。

可能在不远的未来，布局和样式工作会被 AI 取代，但是数据驱动下数据流选型应该比较难以被 AI 取代。

### 再次理解 react + mobx 不如用 vue 这句话

首先这句话很有道理，也很有分量，不过笔者今天将从一个全新的角度思考。

经过前面的探讨，可以发现，现在前端开发过程分为三个部分：副作用隔离 -> 数据流驱动 -> 视图渲染。

先看视图渲染，不论是 jsx、或 template，都是相同的，可以互相转化的。

再看副作用隔离，一般来说框架也不解决这个问题，所以不管是 react/ag/vue + redux/mobx/rxjs 任何一种组合，最终你都不是靠前面的框架解决的，而是利用后面的 redux/mobx/rxjs 来解决。

最后看数据流驱动，不同框架内置的方式不同。react 内置的是类 redux 的方式，vue/angular 内置的是类 mobx 的方式，cyclejs 内置了 rxjs。

这么来看，react + redux 是最自然的，react + mobx 就像 vue + redux 一样，看上去不是很自然。也就是 react + mobx 别扭的地方仅在于数据流驱动方式不同。对于视图渲染、副作用隔离，这两个因素不受任何组合的影响。

就数据流驱动问题来看，我们可以站在更高层面思考，比如将 react/vue/angular 的语法视为三种 DSL 规范，那其实可以用一种通用的 DSL 将其描述，并转换对应的 DSL 对接不同框架（阿里内部已经有这种实现了）。而这个 DSL 对框架内置数据流处理过程也可以屏蔽，举个例子：

```javascript
<button onClick={() => {
  setState(() => {
    data: {
      name: 'nick'
    }
  })
}}>
  {data.name}
</button>
```

如果我们将上面的通用 jsx 代码转换为通用 DSL 时，会使用通用的方式描述结构以及方法，而转化为具体 react/vue/angluar 代码时，就会转化为对应内置数据流方案的实现。

所以其实内置数据流是什么风格，在有了上层抽象后，是可以忽略的，我们甚至可以利用 proxy，将 mutable 的代码转换到 react 时，改成 immutable 模式，转到 vue 时，保持 mutable 形式。

对框架封装的抽象度越高，框架之间差异就越小，渐渐的，我们会从框架名称的讨论中解放，演变成对框架 + 数据流哪种组合更加合适的思考。

## 3 总结

最近梳理了一下 [gaea-editor](https://github.com/ascoders/gaea-editor) - 笔者做的一个 web designer，重新思考了其中插件机制，拿出来讲一讲。

首先大体说明一下，这个编辑器使用 dob 作为数据流，通过 react context 共享数据，写法和 mobx 很像，不过这不是重点，重点是插件拓展机制也深度使用了数据流。

什么是插件拓展机制？比如像 VScode 这些编辑器，都拥有强大的拓展能力，开发者想要添加一个功能，可以不用学习其深奥的框架内容，而是读一下简单明了的插件文档，使用插件完成想要功能的开发。解耦的很美好，不过重点是插件的能力是否强大，插件可以触及内核哪些功能、拿到哪些信息、拥有哪些能力？

笔者的想法比较激进，为了让插件拥有最大能力，这个 web designer 所有内核代码都是用插件写的，除了调用插件的部分。所以插件可以随意访问和修改内核中任何数据，包括 UI。

让 UI 拥有通用能力比较容易，gaea-editor 使用了插槽方式渲染 UI，也就是任何插件只要提供一个名字，就能嵌入到申明了对应名字的 UI 插槽中，而插件自己也可以申明任意数量的插槽，内核中也有几个内置的插槽。这样插件的 UI 能力极强，任何 UI 都可以被新的插件替代掉，只要申明相同的名字即可。

**剩下一半就是数据能力，笔者使用了依赖注入，将所有内核、插件的 store、action 全量注入到每一个插件中：**

```typescript
@Connect
class CustomPlugin extends React.PureComponent {
  render() {
    // this.props.Actions, this.props.Stores
  }
}
```

同时，每个插件可以申明自己的 store，程序初始化时会合并所有插件的 store 到内存中。因此插件几乎可以做任何事，重写一套内核也没有问题，那么做做拓展更是轻松。

其实这有点像 webpack 等插件的机制：

```typescript
export default (context) => {}
```

每次申明插件，都可以从函数中拿到传来的数据，那么通过数据流的 `Connect` 能力，将数据注入到组件，也是一种强大的插件开发方式。

### 更多思考

通过上面插件机制的例子会发现，数据流不仅定义了数据处理方式、副作用隔离，同时依赖注入也在数据流功能列表之中，前端数据流是个很宽泛的概念，功能很多。

redux、mobx、rxjs 都拥有独特的数据处理、副作用隔离方式，同时对应的框架 redux-react、mobx-react、cyclejs 都补充了各种方式的依赖注入，完成了与前端框架的衔接。正是应为他们纷纷将内核能力抽象了出来，才让 redux+rxjs mobx+rxjs 这些组合成为了可能。

未来甚至会诞生一种完全无数据管理能力的框架，只做纯 view 层，内核原生对接 redux、mobx、rxjs 也不是没有可能，因为框架自带的数据流与这些数据流框架比起来，太弱了。

react stateless-component 就是一种尝试，不过现在这种纯 view 层组件配合数据流框架的方式还比较小众。

> 纯 view 层不代表没有数据流管理功能，比如 props 的透传，更新机制，都可以是内置的。

不过笔者认为，未来的框架可能会朝着 view 与数据流完全隔离的方式演化，这样不但根本上解决了框架 + 数据流选择之争，还可以让框架更专注于解决 view 层的问题。

### 从有到无

HTML5 有两个有意思的标签：`details`, `summary`。通过组合，可以达到 `details` 默认隐藏，点击 `summary` 可以 toggle 控制 `details` 下内容的效果：

```html
<details>
  <summary>标题</summary> 
  <p>内容</p> 
</details>
```

更是可以通过 css 覆盖，完全实现 [collapse](https://ant.design/components/collapse-cn/) 组件的效果。

当然就 collapse 组件来说，因为其内部维持了状态，所以控制折叠面板的 打开/关闭 状态，而 HTML5 的 `details` 也通过浏览器自身内部状态，对开发者只暴露 css。

在未来，浏览器甚至可能提供更多的原生上层组件，而组件内部状态越来越不需要开发者关心，甚至，不需要开发者再引用任何一个第三方通用组件，HTML 提供足够多的基础组件，开发者只需要引用 css 就能实现组件库更换，似乎回到了 bootstrap 时代。

有人会说，具有业务含义的再上层组件怎么提供？别忘了 HTML components，这个规范配合浏览器实现了大量原生组件后，可能变得异常光彩夺目，DSL 再也不需要了，HTML 本身就是一套通用的 DSL，框架更不需要了，浏览器内置了一套框架。

> 插一句题外话，所有组件都通过 html components 开发，就真正意义上实现了抹平框架，未来不需要前端框架，不需要 react 到 vue 的相互转化，组件加载速度提高一个档次，动态组件 load 可能只需要动态加载 css，也不用担心不同环境/框架下开发的组件无法共存。前端发展总是在进两步退一步，不要形成思维定式，每隔一段时间，需要重新审视下旧的技术。

话题拉回来，从浏览器实现的 `details` 标签来看，内部一定有状态机制，假如这套状态机制可以提供给开发者，那数据流的 数据处理、副作用隔离、依赖注入 可能都是浏览器帮我们做了，redux 和 mobx 会立刻失去优势，未来潜力最大的可能是拥有强大纯函数数据流处理能力的 rxjs。

当然在 2018 年，redux 和 mobx 依然会保持强大的活力，就算在未来浏览器内置的数据流机制，rxjs 可能也不适合大规模团队合作，尤其在现在有许多非前端岗位兼职前端的情况下。

就像现在 facebook、google 的模式一样，在未来的更多年内，前后端，甚至 dba 与算法岗位职能融合，每个人都是全栈时，可能 rxjs 会在更大范围被使用。

纵观前端历史，数据流框架从无到有，但在未来极有可能从有变到无，前端数据流框架消失了，但前端数据流思想永远保留了下来，变得无处不在。

## 4 更多讨论

> 讨论地址是：[精读《前端数据流哲学》 · Issue #58 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/58)

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