本系列分三部曲:《框架实现》 《框架使用》 与 《数据流哲学》,这三篇是我对数据流阶段性的总结,正好补充以前过期的文章。javascript
本篇是收官之做 《前端数据流哲学》。css
写这篇文章时,颇有压力,若有不妥之处,欢迎指正。html
同时,因为这是一篇佛系文章,因此不会得出你应该用 某某 框架的结论,你应该看成消遣来阅读。前端
首先数据流管理模式,比较热门的分为三种。vue
固然还有第四种模式,裸奔,其实有时候也挺健康的。java
数据流使用通用的准则是:反作用隔离、全局与局部状态的合理划分,以上三种数据流管理模式均可以实现,惟有是否强制的区别。react
一直在思考如何将这三个思惟串起来,后来想通了,按照时间顺序串起来就很是天然。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 的顺序解读这三个框架。
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 的项目,维护的时候须要频繁使用全文搜索,以及至少在两个文件间来回跳跃。
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 找到结合点,前端探索的脚步从未中止。
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 串了起来,还发现了他们内在的关联,这三个思想像一张网,复杂的交织在一块儿。
咱们发现,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 取代。
首先这句话颇有道理,也颇有份量,不过笔者今天将从一个全新的角度思考。
通过前面的探讨,能够发现,如今前端开发过程分为三个部分:反作用隔离 -> 数据流驱动 -> 视图渲染。
先看视图渲染,不管是 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 形式。
对框架封装的抽象度越高,框架之间差别就越小,渐渐的,咱们会从框架名称的讨论中解放,演变成对框架 + 数据流哪一种组合更加合适的思考。
最近梳理了一下 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 会在更大范围被使用。
纵观前端历史,数据流框架从无到有,但在将来极有可能从有变到无,前端数据流框架消失了,但前端数据流思想永远保留了下来,变得无处不在。
若是你想参与讨论,请点击这里,每周都有新的主题,每周五发布。