前端数据流哲学

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

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

1 引言

写这篇文章时,颇有压力,若有不妥之处,欢迎指正。html

同时,因为这是一篇佛系文章,因此不会得出你应该用 某某 框架的结论,你应该看成消遣来阅读。前端

2 精读

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

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

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

数据流使用通用的准则是:反作用隔离、全局与局部状态的合理划分,以上三种数据流管理模式均可以实现,惟有是否强制的区别。react

2.1 从时间顺序提及

一直在思考如何将这三个思惟串起来,后来想通了,按照时间顺序串起来就很是天然。jquery

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

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

最早映入眼帘的是 angular,搬来的 mvvm 思想真是为前端开辟了新的世界,发现代码还能够这么写!虽然 angluar 用起来很重,但 mvvm 带来的数据驱动思想已经愈来愈深刻人心,随后 react 就忽然火起来了。

其实在 react 火起来以前,有一个框架一步到位,进入了 react + mobx 时代,对,就是 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。主要动机是 mobx 手感还不够完美,对于新赋值变量须要用一些 extendObservable 等 api 修饰,正好发现浏览器对 proxy 支持已经成熟,所以笔者后来几乎全部我的项目几乎都用 dob 替代了 mobx。

这一时期三巨头之一的 vue 火了起来,成功利用:若是 ”react + mobx 很好用,那为何不用 vue?“ 的 flag 打动了我。

一直到如今,前端已经发展到可谓五花八门的地步,typescript 战胜 flow 几乎成为了新的 js,出现了 ember、clojurescript 以后,各大语言也纷纷出了到 js 的编译实现,陆陆续续的支持编译到 webassembly,react 做者都弃坑 js 创造了新语言 reason。

以前写过一篇初步认识 reason 的精读

能接下来这一套精神洗礼的前端们,已经养出心里波澜不惊的功夫,小众已经不会成为跨越温馨区的门槛,再学个 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

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

Immutable 具体实现能够参考笔者以前写的一篇精读:精读 Immutable 结构共享

固然,因为很像事件机制的 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 吗?

多提一句,rxjs 对数据流纯函数的抽象能力很是强大,所以前端主要工做在于抽一个工具,将诸如事件、请求、推送等等反作用都转化为数据源。cyclejs 就是这样一个框架:提供了一套上述的工具库,与 dom 对接增长了虚拟 dom 能力。

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

rxjs 带来了两种新的开发方式,第一种是相似 cyclejs,将一切前端反作用转化为数据源,直接对接到 dom。另外一种是相似 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 改变对象也是致使反作用的缘由,笔者认为是,也不是,看以下代码:

obj.a = 1

这段代码在 js 中铁定是 mutable 的?不必定,一样在 c++ 这些能够重载运算符的语言中也不必定了,setter 语法不必定会修改原有对象,好比能够经过 Object.defineProperty 来重写 obj 对象的 setter 事件。

由此咱们能够开一个脑洞,经过运算符重载,让 mutable 方式获得 immutable 的结果。在笔者博客 Redux 使用可变数据结构 有说明原理和用法,并且 mobx 做者 mweststrate 是这么反驳那些吐槽 mobx 缺乏 redux 历史回溯能力的声音的:

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

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

好比 mobx 做者的新做:immer 经过 proxy 元编程能力,将 setter 重写为 Object.assign() 实现 mutable 到 immutable 的转换。

笔者的 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 对框架内置数据流处理过程也能够屏蔽,举个例子:

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

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

因此其实内置数据流是什么风格,在有了上层抽象后,是能够忽略的,咱们甚至能够利用 proxy,将 mutable 的代码转换到 react 时,改为 immutable 模式,转到 vue 时,保持 mutable 形式。

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

3 总结

最近梳理了一下 gaea-editor - 笔者作的一个 web designer,从新思考了其中插件机制,拿出来说一讲。

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

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

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

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

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

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

同时,每一个插件能够申明本身的 store,程序初始化时会合并全部插件的 store 到内存中。所以插件几乎能够作任何事,重写一套内核也没有问题,那么作作拓展更是轻松。

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

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 下内容的效果:

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

更是能够经过 css 覆盖,彻底实现 collapse 组件的效果。

固然就 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

若是你想参与讨论,请点击这里,每周都有新的主题,每周五发布。

相关文章
相关标签/搜索