现代前端框架响应模型对比: Vue, Mobx, React, Redux

18,719 阅读6分钟

现代前端框架的核心是数据状态,也就是 state。一个核心流程是 state 映射到 view,这里是渲染和更新过程,虚拟 dom 在这个过程发挥作用,而如何订阅和处理 state 的变更则是另一个核心流程,也就是响应过程。我们看 React 的名字就是“反应”或者“响应”的意思,Mobx 即 mobile x 也蕴含响应的意思,可见“响应”的重要性。本文将对当前主要前端框架的响应模型做介绍和比较。

React vs Vue

数据状态发生变化时通知方式的区别是目前 React 和 Vue 两大阵营之间的最大差异。

React 的响应的方式最简单直接,代码中通过 setState 触发数据变化,然后会直接通知组件进行更新。跟 React 配套的 Redux 也是一样,state 更改后会直接通知全部相关视图进行更新,而是否真的需要更新则留给视图自己做决定。实际上 React/Redux 的模型就是全体数据统一通知,Redux 是应用的全体数据而 React setState 是组件的全体数据,没有更细的粒度。

Vue 的响应过程则要精细的多,我们可能或多或少都有了解,它的发布订阅模型是精细到数据的每一个属性的,我们详细看一下。

发布订阅模型有两个角色,发布者和订阅者,对前端模型来说,发布者就是 state,订阅者则是渲染函数,或者其他一些响应函数,比如 computed 和 watch 函数。

Vue 需要在两个方面做处理来让他们形成精细的发布订阅关系。通过劫持待发布对象的 get 方法,同时对可能成为订阅者的函数包装 effect 方法,effect 方法会在函数执行的时候将全局 currentEffect 设为当前执行函数,从而在有方法使用了 get 的时候取 currentEffect 为订阅函数,进而存储数据对象、属性和订阅函数之间的关系。再通过劫持 set 方法,就可以在属性变化的时候通过关系找到订阅函数去执行。

这个发布订阅模型要比 React 精细得多,因而 Vue 的性能要比未经优化的 React 好很多,从某种程度上讲,也正是因为 React 的性能问题更突出,才需要有更先进的渲染调度架构 React Fiber 的出现。但从另一个角度看,正是由于 React 没有使用基于 mutable 的响应模型,使得它的数据状态历史更加容易追溯,简单的发布模型,也使得 Redux 中间件成为可能,他们在大型项目中可以定义出更加可维护的逻辑写法,比如 Redux saga。Vue 的作者尤雨溪也表达过这样的意思:Vue 简单方便,适合中小型项目,而 React + Redux 更适合大型项目。

到这里,React/Redux 和 Vue 响应模型的对比就结束了。接下来我们看一下与 Vue 非常接近的另一个框架:Mobx。

Mobx vs Vue

Mobx 与 Vue 的响应模型接近,实际上它是最早把这种细粒度响应模型抽象并且推广开来的框架。

人们把 Mobx 与 React 一起使用,Mobx 来处理发布订阅模型,React 做单纯的视图层。实际上,Mobx 的响应模型可以拿出来做很多事,比如 Formily 用 Mobx 的响应模型实现了复杂表单的高性能,滴滴的跨端框架 CML 的数据层 chameleon-store 用 Mobx 模拟了 Vuex 的 api。

下面我们就来看一下 Mobx 的响应模型细节,我们先看一下 api,Mobx 的 api 基本上能与 Vue 3.0 的 Reactive 模块的 api 对应上,比如:

  • observable => reactive
  • observer => effect
  • computed => computed
  • autorun => watch Mobx 的核心实现思路与 Vue 也大致相同,同样是劫持对象属性的 get set,在订阅函数执行的时候全局记录当前执行函数。不同的是记录发布订阅关系是记录在每个 observable 实例的 $mobx 对象中的,由抽象的 Atom 类型管理。整体思路上有observable 和 derivation 两个类型作为基础的响应逻辑模型,observable 是发布者,derivation 是订阅者。derivation 是 Mobx提出的概念,autorun 和 reaction 继承自 derivation,computed 则是 observable 和 derivation 的综合体,对这些通用概念的系统抽象使得 Mobx 成为一个更通用的响应式框架。注意,Mobx 是一个 functional reactive programing,即函数式响应式编程,并不能说是响应式编程,reactive programing,因为 reactive programing 是特指事件流式的编程形式,在 js 里面的相应实现是 Rxjs。

除了基本的发布订阅之外,Vue 和 Mobx 还需要处理一些其他问题,比如对于他们都有的 computed 概念,触发顺序是一个重要问题。

Computed 之间的依赖关系,是一个图状结构,复杂的依赖关系中,触发的顺序需要保证上层的订阅者最后执行,否则可能造成上层订阅者使用了过期的数据。或者如果无脑执行,会造成上层订阅者被多次触发。

mobx deps

那么如何解决这个问题呢?总共分两步,第一步是让 computed 懒执行,只有在真正有人取它的值的时候才执行,第二步是给 computed 加状态位标识它是否需要重新执行,如果不需要就直接取当前值就好。

Vue 和 Mobx 采取的都是这两个步骤,但是在第二步上略有不同,Vue 只有一个简单的 dirty 标识,Mobx 却有四个状态:NOT_TRACKING,UP_TO_DATE,POSSIBLY_STALE 和 STALE。其中的 STALE 相当于 dirty,POSSIBLY_STALE 主要是为了减少级联订阅关系中的不必要的重计算,因为中间层依赖进行计算后会知道是否真的有改变,如果没有改变上层就不需要计算了。

mobx state

这里如果有兴趣可以看一下这个例子,同一个场景,分别用 VueMobx 实现,观察在下层依赖改变时,级联依赖是否重新执行。可以发现,Vue 版本的 label 在每次 payType 改变时都会被执行,而 Mobx 版本就没有,这就是因为 Mobx 的 computed 如果处在 POSSIBLY_STALE 状态,执行前会再检查一下自己的依赖,如果没有真的改变就不执行了。

除此之外,Mobx 还做了一些其他的优化,比如 transaction,这跟 React 事件处理函数中对 setState 做批量处理的优化是类似的。

到此为止,我们介绍了常见现代前端框架在响应数据变化时做的一些工作,希望大家能够通过我的介绍对现代前端框架的工作模型有更深入的认识。