Skip to content

08列举一种你了解的React状态管理框架

在面试中,如果面试官让你列举一种 React 状态管理框架,你应该如何回答呢?这讲我会带你探讨这个问题。

破题

正如上一讲所提到的:横跨多个层级之间的组件仍然需要共享状态响应变化。传统的状态提升方案难以高效地解决这个问题,比如 Context。

Context 存储的变量难以追溯数据源以及确认变动点。当组件依赖 Context 时,会提升组件耦合度,这不利于组件的复用与测试。

而状态管理框架就很好地解决了这些问题,才得以流行。所以在 React 的开发中,或多或少都会用到状态管理框架。对状态管理框架的掌握程度也就成了面试的必考点

当然,虽然题目要求列举一种,但如果你能说出更多的状态管理框架,那肯定是加分的。这样可以证明你的开发经验足够丰富,且对社区流行很是了解。

承题

当前社区流行的状态管理框架有哪些呢?莫过于 Flux、Redux、Mobx 了。在回答这些框架本身的内容之外,你最好可以结合个人的思考,补充一些个人观点

这样初步的框架就出来了。

入手

Flux

Flux 给我的印象就是平地一声雷,有种"天不生 Flux,状态管理如长夜"的感觉。回顾历史,你会发现 Flux 如同 React 一样对业界影响巨大。如果你对 Flutter 与 SwiftUI 有了解的话,就能理解 React 对后起之秀的那种深远影响。Flux 同样如此,它提出了一种 MVC 以外的成功实践------单向数据流,这同样深远地影响了"后来人"。

2014 的 Facebook F8 大会上提出了一个观点,MVC 更适用于小型应用 ,但在面向大型前端项目时,MVC 会使项目足够复杂 ,即每当添加新的功能,系统复杂度就会疯狂增长。如下图所示,Model 与 View 的关联是错综复杂的,很难理解和调试,尤其是 Model 与 View 之间还存在双向数据流动

这对于接手老代码的人来说是个令人头疼的难题,因为他们害怕承担风险,所以不敢轻易修改代码。这也正是 MVC 模式被 Facebook 抛弃的原因。

所以他们提出了一种基于单向数据流的架构。如下图所示:

我先解释下上图中涉及的概念。

  • View是视图层,即代码中的 React 组件。

  • Store是数据层,维护了数据和数据处理的逻辑。

  • Dispatcher是管理数据流动的中央枢纽。每一个 Store 提供一个回调。当 Dispatcher 接收一个 Action 时,所有的 Store 接收注册表中的 Action,然后通过回调产生数据。

  • Action可以理解为一种事件通知,通常用 type 标记。

具体的流程是这样的,Store 存储了视图层所有的数据,当 Store 变化后会引起 View 层的更新。如果在视图层触发 Action,比如点击一个按钮,当前的页面数据值会发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层。由于 Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,会触发 View 层更新。

从应用场景来看,Flux 除了在 Facebook 内部大规模应用以外,业界很少使用。因为它的概念及样板代码相比后起之秀,还是有点多。从如今的视角看,Flux 可称为抛砖引玉的典范,开启了一轮状态管理的军备竞赛。

Redux

这场军备竞赛的佼佼者非 Redux 莫属。当提到 Redux,不得不提 Elm 这样一种传奇的语言。Elm 虽然是一种语言,但实际上主要用于网页开发,它设计了一种 Model、View、Message、Update 的更新思路。

Elm 有着这样独特的设计

  • 全局单一数据源,不像 Flux 一样有多个 Store;

  • 纯函数,可以保证输入输出的恒定;

  • 静态类型,可以确保运行安全。

在 Redux 的文档中,毫不避讳地提到,自己借鉴了这个设计。

在 Flux 与 Elm 的基础上,Redux 确立了自己的"三原则"。

  • 单一数据源,即整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 Store 中。

  • 纯函数 Reducer,即为了描述 Action 如何改变状态树 ,编写的一个纯函数的 Reducer。

  • state 是只读的,唯一可以改变 state 的方法就是触发 Action,Action 是一个用于描述已发生事件的普通对象。

这三大原则使 Redux 的调试工具实现了时间回溯功能,通过录制回放 Action,完整重现了整个应用路径,这在当时引发了不小的轰动。下图中的 Redux DevTools 是开发中常用的 Redux 调试工具,界面中展示的内容就是时间回溯功能,可以查看每个操作对全局 Store 产生的变化。

Redux 调试工具的时间回溯

在 Redux 社区中有一个经久不衰的话题,就是如何解决副作用(Side Effect)。在 16 年到 18 年之间,即使是在知乎,这也是前端最火的话题。每隔一段时间社区就会冒出一个新的方案,宣称对副作用有了最佳实践。

Redux 团队是这样论述"副作用"的:任何具备业务价值的 Web 应用必然要执行复杂的逻辑,比如 AJAX 请求等异步工作,这类逻辑使函数在每次的执行过程中,产生不同的变化,这样与外界的交互,被称为"副作用"。一个常见的副作用的例子是这样的,你发一个网络请求,需要界面先显示 Loading,再根据请求是否成功,来决定显示数据还是显示报错信息,你会发现在整个过程中,异步操作在 Redux 中无从添加,因为 Redux 本身深受函数式编程的影响,导致:

  • 所有的事件都收拢 Action 去触发;

  • 所有的 UI 状态都交给 Store 去管理;

  • 所有的业务逻辑都交由 Reducer 去处理。

在这里 Action、Reducer 是纯函数,Store 也只是一个 state 状态树,都不能完成处理副作用的操作。

真正可以解决副作用的方案主要分为两类:

  • 在 Dispatch 的时候有一个middleware 中间件层 ,拦截分发的Action并添加额外的复杂行为,还可以添加副作用;

  • 允许 Reducer 层直接处理副作用。

你可以看出,这两类方案并没有把副作用从代码中消除,而是通过不同的方式转嫁到不同的层级中。对于每一类我们都看一下主流的解决方案。

第一类中,流行的方案是 Redux-thunk,其作用是处理异步 Action,它的源码在面试中经常被要求独立编写。

java
function createThunkMiddleware(extraArgument) {
	  return ({ dispatch, getState }) => (next) => (Action) => {
	    if (typeof Action === 'function') {
	      return Action(dispatch, getState, extraArgument);
	    }
	
	    return next(Action);
	  };
	}
	
	const thunk = createThunkMiddleware();
	thunk.withExtraArgument = createThunkMiddleware;
	
	export default thunk;

如上代码所示,Redux-thunk 通过添加中间件来判断 Action 是否为函数:

  • 如果是函数,则 Dispatch,将当前的整个 state 以及额外参数传入其中;

  • 否则就继续流转 Action。

这是一个最早最经典的处理 Redux 副作用的方案,你还可以自己去自定义 Store 的 middleware。那如果 Action 是一个数组,或者是一个 promise ,该怎么处理呢?这都可以实现。因为社区中 Action 可以是数组,可以是 promise,还可以是迭代器,或者 rxjs,比如 Redux-Saga、Redux-Promise、Redux-Observable 等。

第二类方案相对冷门很多,但从设计上而言,思考得却更加深刻。比如 Redux Loop 就深入地借鉴了 Elm。在 Elm 中副作用的处理在 update 层,这样的设计叫分形架构。如下代码所示:

java
import { loop, Cmd } from 'redux-loop';
function initAction(){
    return {
      type: 'INIT'
    };
}
function fetchUser(userId){
    return fetch(`/api/users/${userId}`);
}
function userFetchSuccessfulAction(user){
   return {
      type: 'USER_FETCH_SUCCESSFUL',
      user
   };
}
function userFetchFailedAction(err){
   return {
      type: 'USER_FETCH_FAILED',
      err
   };
}
const initialState = {
  initStarted: false,
  user: null,
  error: null
};
function Reducer(state = initialState, Action) {
  switch(Action.type) {
  case 'INIT':
    return loop(
      {...state, initStarted: true},
      Cmd.run(fetchUser, {
        successActionCreator: userFetchSuccessfulAction,
        failActionCreator: userFetchFailedAction,
        args: ['123']
      })
    );
  case 'USER_FETCH_SUCCESSFUL':
    return {...state, user: Action.user};
  case 'USER_FETCH_FAILED':
    return {...state, error: Action.error};
  default:
    return state;
  }
}

那什么是分形架构呢?这就需要提到分形架构:

如果子组件能够以同样的结构,作为一个应用使用,这样的结构就是分形架构。

分形架构的好处显而易见,复用容易、组合方便。Redux Loop 就做出了这样的尝试,但在实际的项目中应用非常少,因为你很难遇到一个真正需要应用分形的场景。在真实的开发中,并没有那么多的复用,也没有那么多完美场景实践理论。

虽然 Redux Loop 在分形架构上做出了探索,但 Redux 作者并不是那么满意,他甚至写了一篇长文感慨,没有一种简单的方案可以组合 Redux 应用,并提了一个长久以来悬而未决的 issue

最后就是关于 Redux 的一揽子解决方案。

  • 在国外社区流行的方案是rematch ,它提供了一个标准的范式写 Redux。根据具体的案例,你会发现 rematch 的模块更为内聚插件更为丰富

  • 而国内流行的解决方案是 dva

关于这两个方案,你可以参考我给的代码进行学习,这里就不详细讲解了。

Mobx

从 Redux 深渊逃离出来,现在可以松口气了,进入 Mobx。如果你喜欢 Vue,那么一定会爱上 Mobx;但是如果喜欢 Redux,那你一定会恨它。不如先抛下这些,看看官方示例:

java
import {observable, autorun} from 'mobx';
var todoStore = observable({
    /* 一些观察的状态 */
    todos: [],
    /* 推导值 */
    get completedCount() {
        return this.todos.filter(todo => todo.completed).length;
    }
});
/* 观察状态改变的函数 */
autorun(function() {
    console.log("Completed %d of %d items",
        todoStore.completedCount,
        todoStore.todos.length
    );
});
/* ..以及一些改变状态的动作 */
todoStore.todos[0] = {
    title: "Take a walk",
    completed: false
};
// -> 同步打印 'Completed 0 of 1 items'
todoStore.todos[0].completed = true;
// -> 同步打印 'Completed 1 of 1 items'

Mobx 是通过监听数据的属性变化,直接在数据上更改来触发 UI 的渲染。是不是一听就非常"Vue"。那 Mobx 的监听方式是什么呢?

  • 在 Mobx 5 之前,实现监听的方式是采用 Object.defineProperty;

  • 而在 Mobx 5 以后采用了 Proxy 方案。

答题

通过以上的学习,我们可以答题了。

首先介绍 Flux,Flux 是一种使用单向数据流的形式来组合 React 组件的应用架构。

Flux 包含了 4 个部分,分别是 Dispatcher、 Store、View、Action。Store 存储了视图层所有的数据,当 Store 变化后会引起 View 层的更新。如果在视图层触发一个 Action,就会使当前的页面数据值发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层,Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,触发 View 层更新。

Flux 的优点是单向数据流,解决了 MVC 中数据流向不清的问题,使开发者可以快速了解应用行为。从项目结构上简化了视图层设计,明确了分工,数据与业务逻辑也统一存放管理,使在大型架构的项目中更容易管理、维护代码。

其次是 Redux,Redux 本身是一个 JavaScript 状态容器,提供可预测化状态的管理。社区通常认为 Redux 是 Flux 的一个简化设计版本,但它吸收了 Elm 的架构思想,更像一个混合产物。它提供的状态管理,简化了一些高级特性的实现成本,比如撤销、重做、实时编辑、时间旅行、服务端同构等。

Redux 的核心设计包含了三大原则:单一数据源、纯函数 Reducer、State 是只读的。

Redux 中整个数据流的方案与 Flux 大同小异。

Redux 中的另一大核心点是处理"副作用",AJAX 请求等异步工作,或不是纯函数产生的第三方的交互都被认为是 "副作用"。这就造成在纯函数设计的 Redux 中,处理副作用变成了一件至关重要的事情。社区通常有两种解决方案:

第一类是在 Dispatch 的时候会有一个 middleware 中间件层,拦截分发的 Action 并添加额外的复杂行为,还可以添加副作用。第一类方案的流行框架有 Redux-thunk、Redux-Promise、Redux-Observable、Redux-Saga 等。

第二类是允许 Reducer 层中直接处理副作用,采取该方案的有 React Loop,React Loop 在实现中采用了 Elm 中分形的思想,使代码具备更强的组合能力。

除此以外,社区还提供了更为工程化的方案,比如 rematch 或 dva,提供了更详细的模块架构能力,提供了拓展插件以支持更多功能。

Redux 的优点很多:结果可预测;代码结构严格易维护;模块分离清晰且小函数结构容易编写单元测试;Action 触发的方式,可以在调试器中使用时间回溯,定位问题更简单快捷;单一数据源使服务端同构变得更为容易;社区方案多,生态也更为繁荣。

最后是 Mobx,Mobx 通过监听数据的属性变化,可以直接在数据上更改触发UI 的渲染。在使用上更接近 Vue,比起 Flux 与 Redux 的手动挡的体验,更像开自动挡的汽车。Mobx 的响应式实现原理与 Vue 相同,以 Mobx 5 为分界点,5 以前采用 Object.defineProperty 的方案,5 及以后使用 Proxy 的方案。它的优点是样板代码少、简单粗暴、用户学习快、响应式自动更新数据让开发者的心智负担更低。

当然利用好知识导图,更有利于知识的学习。

关于个人观点部分,你可以参考我以下观点。这里务必谨记,表达个人观点时,切忌对技术栈踩一捧一,容易引发面试官反感。

我认为 Flux 的设计更偏向 Facebook 内部的应用场景,Facebook 的方案略显臃肿,拓展能力欠佳,所以在社区中热度不够。而 Redux 因为纯函数的原因,碰上了社区热点,简洁不简单的 API 设计使社区疯狂贡献发展,短短数年方案层出不穷。但从工程角度而言,不是每一个项目都适用单一数据源。因为很多项目的数据是按页面级别切分的,页面之间相对隔绝,并不需要共享数据源,是否需要 Redux 应该视具体情况而定。Mobx 在开发项目时简单快速,但应用 Mobx 的场景 ,其实完全可以用 Vue 取代。如果纯用 Vue,体积还会更小巧。

进阶

在现场面试中很容易让你徒手实现一个 Redux。在实现 Redux 的时候,需要注意两个地方。

  • createStore。即通过 createStore,注入 Reducer 与 middleware,生成 Store 对象。

  • Store 对象的 getState、subscribe 与 dispatch 函数。getState 获取当前状态树,subscribe 函数订阅状态树变更,dispatch 发送 Action。

在实现上尽量采用纯函数的实现方案。如果没有思路建议阅读 Redux 4.x 的代码

总结

一涉及状态管理,内容就相当多了,在面试中也是非常浓厚的一笔。对于本讲梳理的知识导图请务必掌握,最好可以自己画一画;本讲中提到的代码也都要看一看,可以加深对概念的理解。因为大厂中的前端项目结构的复杂性高,一定会使用状态管理框架来规范模块划分与人工操作。所以在面试前,一定要确保能够完全消化本讲中的内容。

下一讲开始为你讲解渲染流程的内容,会相对轻松一些。

[

](https://shenceyun.lagou.com/t/mka)

《大前端高薪训练营》

对标阿里 P7 技术需求 + 每月大厂内推,6 个月助你斩获名企高薪 Offer。点击链接,快来领取!