近期准备开发一个数据分析 SDK,定位是作为数据中台向外输出数据分析能力的载体,前端的功能表现类似低代码平台的各种拖拉拽。作为中台能力的载体,SDK 未来很大概率会需要支持多种视图层框架,比如Vue2/Vue3/React等。所以在技术架构上对视图层框架的依赖性越轻,迭代的成本越低。基于这样的目标,本文对前端状态管理工具进行调研,在技术选型上应当尽量减轻与视图框架的绑定程度,理想的目标是构建与视图框架无关的数据/状态管理层

调研对象包括以下:

RxJS 跟状态管理没有任何关系,不过它天生适合编写异步和基于事件的程序,有了这个前提,完全可以封装一套基于 RxJS 的状态管理架构,比如 Akita。同时RxJS 非常适合用来管理事件流,如果状态管理工具能够与 RxJS 比较好的搭配使用,能够达到事半功倍的效果。本文的调研不涉及 RxJS,此处只列举出它的作用,不做细节调研。

通过以下几个维度进行对比(排名分先后):

业务场景覆盖率

在进入调研之前,首先把数据分析 SDK (下文简称分析 SDK)的定位以及业务场景讲清楚。

分析 SDK 是数据分析功能的承担者,但 SDK本身不是一个可用的完整产品,上层需要一个分析平台作为宿主载体。以网易有数为例,网易有数是一个大数据平台,它包含很多功能,比如看板、报告定制、取数、多维分析等等。分析 SDK 在其中的角色就是这些功能的载体,网易有数平台将分析 SDK 集成进来,以可视化的形式提供分析相关的功能。下图展示的是网易有数报告定制的页面:

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

数据分析是一种类似的低代码的业务场景,从技术角度上有以下特征:

  • 数据量大且结构复杂;
  • 事件流复杂、高频且时序敏感;
  • 组件结构复杂且互相之间存在大量的数据共享。

除此之外,之所以要做一个分析 SDK而不是分析平台,是因为中台的服务产出需要支持前台业务的接入,分析 SDK 的目标用户是前台业务部门的各个分析平台。在这个前提下,分析 SDK 对技术有另外一个额外要求:

  • 支持前台分析平台的技术栈。

而前台不止一个,不同业务线的技术栈也不统一,前端技术栈的契合度主要是组件要支持多种视图层框架,比如React、Vue2、Vue3甚至Angular,所以分析 SDK的技术架构应当尽量减轻对视图层框架的依赖程度,将核心业务逻辑从视图层抽离出来。这样的好处能够降低支持不同视图层框架时所投入的人力成本、时间成本和维护成本。这就是本文开头所说的为什么要构建与视图框架无关的数据/状态管理层的原因。「Smart Modeling,Dumb View」,即逻辑集中在数据层,视图层尽量只做展示(类似 React 的 Pure Component)。

结合分析 SDK的定位和业务场景,可以提炼出对状态管理工具的几点具体需求:

  • 支持 state 按子模块划分命名空间,以便支持复杂数据模型;
  • 与视图层框架没有强绑定关系,以便支持多种视图层框架;
  • 支持时间回溯,比如undo/redo,这是编辑器的基本操作。

数据流

目前绝大多数状态管理工具都遵循单向数据流原则,这项原则最早是由 Flux 带入到前端领域,所以在了解各个状态管理工具的数据流之前,有必要先搞清楚 Flux 架构。

Flux

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

  • Action是描述一个行为的对象,每个 action 里都包含两部分信息:actionType 和 payload,分别代表行为的类型和携带的数据;
  • Dispatcher是一个调度中心,它是 action 和 store 的连接中心。有两个核心方法:
    • dispatch方法:触发一个 action,一般由 view 层调用;
    • register方法:用于注册 actionType 的回调,在回调中操作 store。
  • Store是数据仓库以及数据操作的唯一场所,当数据发生变化时,向外广播change事件;
  • View层监听 store 的change事件,调用setState方法来更新相应的组件状态。

Flux 并没有规定如何进行异步操作,比如接口的网络请求,这种场景在前端应用中非常高频。为了解决这种问题,有人提出了Action Creators的概念并在开发者社区获得了认同,加入 Action Creator 之后的 Flux 数据流如下:

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

Action Creators 顾名思义就是 action 的「制造者」,在“制造”action之前可以进行任何行为,包括异步操作。比如以下代码:

async function updateUserInfoCreator(){
 // 第一步:异步获取用户信息
  const info = awaitfetch('...');
  //第二步:触发action
  dispatch({
actionType: 'UPDATE_USERINFO',
    data: info
  });
}

在 view 层调用 updateUserInfoCreator 函数即可。

Flux 的价值更多的体现在理论而非实用意义上,它提出的单向数据流模式被后续很多优秀的状态管理工具借鉴。Facebook 提供了一个 Flux 实现,不过目前很少有开发者直接使用它,而是使用一些在 Flux 架构模式基础上的改进方案,最广泛的就是 Redux。

Redux

Redux 遵循 Flux 的单向数据流思想,与 Flux 不同的是,Redux 没有 Dispatcher 的概念,而是将本属于 Dispatcher 的 dispatch 方法内聚到了 Store 对象上。

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

  • Redux 中 Action 的定位与 Flux 一致,都是描述一个具体行为的对象,包括 actionType 和 payload 两部分信息;
  • Redux 中的 Store 只有一个,也就是单一数据源,并且所有 state 共同组成了一个树形结构。这与 Flux 不同,Flux 并没有规定 Store 的个数限制以及 state 的组合方式,可以一个 store 对应所有 view,也可以每个 view 分别对应一个 store;
  • Reducer 是 Redux 的专有概念,响应 action 并返回更新后的 state 发送到 store 中。Reducer 并不会修改 state,而是在 action 和 old state 基础上计算出新的 state 值并发送给store,没有任何副作用,更新 state 的行为是由 store 内部进行,对开发者不可见。

与 Flux 一样,Redux 同样没有规定如何处理异步数据流。最原始的方案是上文提到的 Action Creators。不过 Redux提供了更优雅的解决方案:中间件

Redux 中间件同时也扩充了 dispatch 的行为,只要保证经过多个中间件的一系列连续行为的最后 dispatch 返回一个规范的 action对象即可,中间过程中可以 dispatch 一个 action creator 函数,这个函数内部还可以再次 dispatch 另一个 action。

中间件极大的丰富了 Redux 的可扩展性,孵化出很多优秀的异步数据流解决方案,比如 redux-thunk、redux-saga 等等。

综合以上,Redux 的优势有以下几点:

  • 单向数据流;
  • 单一数据源,state聚合方便支持时间回溯;
  • 引入数据不可变性理念( Reducer 本质是一个纯函数),state 写行为收敛;
  • 可扩展性高。

Redux 同时也有一些被人诟病的缺点,主要有两个:

  • 繁琐。各个角色分配的很精细,这样的优点是行为单一逻辑严谨,缺点是样板代码(boilerplate code)数量大,尤其是 reducer,在复杂场景中需要写大量的switch,可读性差维护难度大,要理解一个状态机往往需要配合好几个代码文件才能读懂;
  • 复杂。Redux 默认只支持同步数据流,提供中间件机制让开发者自己定制异步数据流,社区中的解决方案复杂度不一,复杂业务场景下的解决方案比如 redux-saga 的复杂度更是高出几个量级,导致开发者在做技术选型和写代码时很头疼。

另外,Redux 是函数式编程的推崇者,架构和API 设计对喜欢函数式编程的开发者非常友好。

Vuex

Vuex 是针对 Vue 的一种特异化的 Flux,保留了单向数据流的核心概念,同时吸取了部分 Redux 的理念。跟 Flux/Redux相比,Vuex 是更加全面的状态管理解决方案,提供了异步操作支持,见下图:

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

由于 Vuex只能搭配 Vue 使用,不具备适配多种视图框架的能力,所以详细的解析就不写了,只简单提一下它的优缺点。

Vuex的优点在于:

  • 全面。Vuex 天然支持异步数据流,开发者不必操心选型问题。当然这个也有两面性,如果存在 Vuex 覆盖不了的业务场景(虽然概率很小),那么很大可能造成包括视图层技术栈的整体迁移;
  • 易用。包括两方面,一是 API足够简单,$store 对象直接挂载到 Vue 实例对象上;二是各角色划分清晰且职责单一( action 相对复杂一些),容易理解;
  • 响应式。与 Vue 的响应式特性天然结合,配合简单易用的 API,Vue + Vuex 的组合在写代码时非常舒服。

Vuex 的缺点,仅个人理解,有以下几点:

  • 适用面窄。与 Vue 强绑定,不适用于其他视图框架。当然这也是跟它自身的定位有关,Vuex 本来就是针对 Vue 的解决方案,并没有想成为一种通用方案。这算不上缺点,但在数据分析 SDK这个需求背景下确实不合适;
  • 繁琐。与 Redux 一样,样板代码过多造成阅读代码的难度加强。

Vuex 和 Redux 本质上都是基于 Flux 的改进方案,核心思想同属于 Flux 体系。接下来几个是另外一套体系:响应式

Mobx

Mobx 是一个基于函数式响应式编程(Functional Reactive Programming,简称 FRP)的状态管理解决方案,同样遵循单向数据流原则。

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

理解 Mobx 之前,必须先搞清楚两个核心概念:observableobserver。从名字上很好理解,observable 是可被观察的对象,observer 是观察者。这两个概念被广泛地使用在发布订阅模式(Pub/Sub Pattern)、观察者模式(Observer Pattern)以及响应式编程中。observable 对象的变化会“自动触发” observer 对象执行对应的响应逻辑,而自动触发的实现方式在不同的工具中存在差异,进而造成代码范式、扩展性、性能等方面的差异。

在此背景之下,再去理解Mobx中的三个核心概念:

  • State - 状态,顾名思义就是应用程序使用的状态数据,在 Mobx 中,state 是一种 Observable 对象

  • Actions - 动作,是任何一段可以改变 state 的代码。Mobx中的 action 与 Redux 和 Vuex 中的 action 都不同,Redux 的action 是行为的描述对象,并不会改变 state;Vuex 中的 action 是可选的,一般是用来执行异步操作,而改变 state 的执行者是 mutation。Mobx 中 action 其实是一个抽象概念,action 的目的是修改 state,至于如何修改完全交给开发者自由发挥,也就是说 action 中可以包含任意逻辑的代码,包括异步操作(action.bound/runInAction/flow);

  • Derivations - 衍生。Mobx 官方对 derivation 的定义如下:
    Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

    有些不好理解,其实可以简单粗暴的把 derivation 理解成 Observer 对象,它对 state 的改变做出响应,但是有一个条件,derivation只能消费 state,不能对 state 进行再次操作。

    Derivation 分成两种:

    • Computed values - 计算值,类似 Vue 中的 computed value,基于 state 使用一个纯函数计算出另外一个值;
    • Reactions -反应,根据 state 改变自动触发的一些副作用,比如渲染 UI。

    至于什么时候用 Computed 什么时候用 Reaction 其实并没有绝对的边界,Reaction 可以解决绝大多数问题只不过需要自己写代码模拟 Computed(这一点也跟 Vue 中的 watch 和 computed 类似),如果应用中需要一个基于 state 的派生值同时这个值有一定的复用性,可以考虑使用 Computed。

Mobx的优点是,它没有 Redux 和 Vuex 中那么多概念,样板代码的数量很小,代码可读性和可维护性高。基于这一点,Mobx 的上手也非常容易,开发者需要理解的概念越少越容易上手。此外,虽然 Mobx 是基于 FRP,但它的 API设计非常适合面向对象编程,比如 decorator 装饰词、class 是一等公民等等。而且有 Reactions 作为响应式编程和命令式编程的桥梁,开发者可以用更舒服的范式写代码。

Mobx 虽然不是 Flux 系列,但有一点与 Flux 的设计理念相同:没有规范 store 的组织模式。Redux 和 Vuex 中的 store 都是树形结构,单一数据源并且方便快照(snapshot),这个优点让两者非常便于调试,并且支持时间回溯的场景上游刃有余。而 Mobx 并没有这些优点,所以社区中涌现了一批补充方案,比如mobx-state-tree(简称MST)和mobx-keystone,核心思想就是将 store 的组织结构聚拢为树状,以便支持更友好的调试和时间回溯。

综合以上,总结 Mobx的优点是:

  • 简单易学习;
  • 代码可读性和可维护性高;
  • OOP编码范式,易于上手。

缺点有以下:

  • 调试困难(与Redux/Vuex相比);
  • 不支持时间回溯。

搭配使用一些社区解决方案,Mobx的以上缺点可以得到一定程度上的弥补。

有很多开发者认为 Redux更适合复杂的大型应用,Mobx 适合数据流相对简单的应用。这其实并不绝对。Redux 严谨的代码组织确实对复杂场景有加分,但 Mobx 也并不是不适用,而且市场中已经有很多 Mobx 解决复杂场景的实践经验,比如低代码开发平台Mendix,而且 Mendix 的业务场景与数据分析有很高的相似度,这也从侧面证明了 Mobx 与此类低代码场景的契合度。

Akita

Akita是来自 Datorama 公司研发团队的前端状态管理方案,跟其他几个竞品相比,Akita 的资历是最浅的。不过 Akita有一个优势:与 Mobx一样,Akita 经过了与数据分析类似业务场景的实践验证。Datorama 是一家提供数据解决方案的公司,它的业务场景与数据分析有很大的重合度,Akita 已经在 Datorama 的一些产品中得到了实践和验证。

Datorama 是 Angular 技术栈,Akita 最初就是为了解决Angular的状态管理,后期开源后已经从 Angular 技术栈中剥离,对视图层框架没有强依赖关系。不过仍然保留了最核心的一点:基于 RxJS。在前端三大框架中,Angular 与 RxJS 的关系最紧密,Akita 最早作为 Angular 的状态管理方案也对 RxJS 有强依赖,包括数据的封装也是遵循 RxJS的“万物皆流”的理念。

下图是 Akita 的数据流:

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

同样是单向数据流,Akita 有以下几个角色:

  • Store - 仓库。Akita 中 Store 分为两种:Basic Store 和 Entity Store。Basic Store 指的是常规的状态数据,一般是一个纯 JavaScript 对象,没有额外附加的 API;Entity Store即实体 Store,Akita 将 Entity Store 类比为关系型数据库的表(table),有一系列配套的 CURD API;
  • Query - 查询。Akita 限制 Store 中的数据不能被直接读取,必须借助一个 Query 对象做桥接(类似 Vuex的 Getter )。与 Store 一样,Query 也分为 Basic Query 和 Entity Query,两者的主要区别是 API 的丰富程度,Basic Query 只有基本的条件查询和全量查询 API,Entity Query 搭配 Entity Store 使用,有更丰富的 API。另外,如图中所示,Query 可以嵌套;
  • Component - 组件,即视图层;
  • Service - 服务。Akita 中的 Service 与 Mobx 的 Action 有些类似,都是为了封装更新 Store的逻辑,包括异步操作。

Akita 与 Mobx/Flux 有一个相同的设计:没有规范 Store 的组织模式。而且由于比较年轻,生态不繁荣,社区并没有类似 MST 的解决方案,这造成在面对复杂数据场景下没有既定的范式可遵循,代码的健壮性非常依赖开发者的能力。Akita 的 Entity Store 是一个 class,可以将其他 Store 作为一个 property,以此来实现Store 的嵌套。但 JavaScript 对象之间复杂的引用关系很容易造成 memory leak,这同样是对开发者自身能力的高要求。

Akita的优点主要有三个:

  1. 足够简单,核心概念比 Mobx 和 Flux 还少,对开发者来说,有足够的定制空间。同时如上文所述,这是一把双刃剑,对开发者的能力要求很高;
  2. 与关系型数据库搭配顺畅。Akita 的概念设计与关系型数据库非常相似,这可能也是结合 Darorama 的业务特色,数据分析场景中的数据模型一般是一张二维表,Akita 的实体概念与 table 的搭配非常自然;
  3. 与 RxJS 无缝衔接。数据分析业务场景的事件流操作非常适合用 RxJS,Akita 底层基于 RxJS,这一点是其他竞品没有的优势。

Akita 的缺点,如上文所述,有以下几个:

  1. 对开发者的编码能力要求很高;
  2. 社区不繁荣,生态不够健全,没有在市场中得到大范围实践验证;
  3. 比较小众,遇到问题可交流和参考的空间很小。

小结

综合以上,各工具的基本情况如下:

与视图框架的绑定程度和改造成本

除了 Vuex 之外,其他几个工具都没有限制视图层框架,只不过 Vue + Vuex 生态比较健全,使用其他状态管理工具的情况比较少。Redux/Mobx/Akita 目前对 React的支持都很好,要配合 Vue 框架,为了提高编码效率和维护成本,一般需要一定的改造成本。

由于 Vuex 不具备兼容多种视图层框架的能力,所以下文的各维度对比不再统计 Vuex。

性能

数据分析业务场景需要处理的数据量远大于常规 Web 应用,不过以这种数据的量级还远未达到需要对工具性能要求非常苛刻的程度,所以对于性能的对比仅做参考。

这里的意思并不是说工具性能不重要,而是还没有成为应用程序的性能瓶颈。本文调研的几个工具都是很成熟的开源产品,已经经受了大量的业务验证,虽然在性能上存在些许差距,但对应用性能表现的影响非常有限。

性能对比

以下参考自社区的两份性能对比数据,一份是对比 Redux 和 Mobx,一份是对比 Redux 和 Akita。两份数据都是配合 React 完成。

Redux vs Mobx

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

这张表格来自 Mobx 作者 Michel Weststrate 的实验数据,场景是在包含不同数量级 items 的 todolist 应用上进行增/改操作,分别统计 Redux 和 Mobx 的耗时情况。从表格里可以看出 Mobx 有明显的性能优势。

Redux vs Akita

Redux/Mobx/Akita/Vuex对比 - 选择更适合低代码场景的状态管理方案-LMLPHP

这份数据来自Performance Comparison of State Management Solutions in React,场景是生成一个 grid,每次生成100行,每行10列,然后随机更新1000列的数据。测试进行10次,统计总耗时(单位s)。从上图中可以明显看出 Akita 的耗时远大于 Redux,更新行为的耗时对比尤其明显。Akita 毕竟比较“年轻”,很多方面赶不上老大哥 Redux 也很正常。

综合上面的两份实验数据,可以得出结论:性能方面 Mobx > Redux > Akita

之所以上述实验仅做参考,一方面是因为实验的场景与真实的业务场景差距很大,现实业务中不可能只用 Redux 或 Mobx,往往还需要配合其他解决方案,比如 redux-thunk 或 MST;另一方面是实验本身并不绝对严谨,而且由于是搭配 React 进行,React 本身在不同逻辑场景下的性能表现也会直接影响实验结果。

不过第二个实验中涉及的一个点对本次调研工作非常有价值:状态管理工具的批量(batch)更新能力

批量更新

数据分析是重交互、重通信的事件密集型业务场景,很大可能在非常短的时间内发生多个事件,如果每个事件都触发一次渲染流程(包括计算逻辑和渲染行为)的话,不仅会产生非常严重且无价值的性能损耗,而且如果涉及网络请求的话还可能产生行为时序混乱进而造成结果的不正确,对业务产生无法预估的负面影响。

所谓批量更新是一个笼统的说法,在不同的工具中有不同的术语表达,不过核心目的是统一的,都是将一定时间内的 store 更新行为进行归拢,消除中间态只产生最终结果。这种技术手段在前端还有另外一个叫法:防抖(debounce)。

Redux 本身并不具备批量更新能力,需要搭配社区解决方案,比如 redux-batched-subscriberedux-batched-actions

Mobx 有一个底层 API:transaction。这是个函数,作用是将一段时间内的所有更新行为按时序进行批量处理,所有行为处理完成之后才会通知 observer 执行回调,中间过程中不会产生任何回调。很明显 transaction 是借鉴的数据库中的事务概念。

Akita 与 Redux 一样,本身同样不具备批量更新的能力,但是由于它的底层基于 RxJS,可以使用 RxJS 的所有能力,在处理防抖场景下常用sampleTimedebounceTime两个方法。

学习曲线

风险与隐患

对于开源工具的选择需要考虑的风险与隐患主要考虑其社区、生态以及背后团队的响应及时性

从这三个角度上对比,Redux 作为资历最老的一个,各方面也是最好的,虽然目前有大量开发者对 Redux 的吐槽,但总的来说 Redux 的位置仍然很稳固,尤其是在 React社区。

Mobx 是近两三年才逐渐占据主流,社区和生态也相对比较完善,开发团队的响应速度也不错。

Akita 跟前两者比起来,最大的优势就是开发团队响应速度很快,目前处于上升期,开发团队也比较活跃。但相对来说还是比较小众,Akita 最早是面向 Angular 的,Angular 的开发者群体规模本身就比 React 和 Vue 小。 Akita 底层的 RxJS 更加小众(虽然很好用)。目前围绕 Akita 的复杂业务场景除了 Darorama 公司自己的业务之外,还没有其他比较好的实践验证。

结论

综合以上所有的调研维度,可以得出以下结论:

综合对比,在 Redux/Mobx/Akita 三者当中,数据分析 SDK 的状态管理技术选型是:Mobx

参考资料

  1. Flux架构的工作原理
  2. 为 MobX 开启 Time-Travelling 引擎
  3. Build A View-Framework-Free Data Layer Based on MobX — Integration With Vue
  4. 用mobx构建大型项目的最佳实践
  5. Becoming fully reactive: an in-depth explanation of MobX;
  6. 现代前端框架响应模型对比: Vue, Mobx, React, Redux
  7. Mobx vs Reactive Stream Libraries
  8. Defining data stores with Mobx;
  9. OOP and RxJS: Managing State in React with Akita
06-09 20:00