网络日志

热门前端状态管理方案选择:Redux/Mobx/Recoil

对于前端开发来说,React的状态管理方案选择一直是核心问题之一。由近年的下载量比对图来看,最受欢迎的三种方案依次是Redux、mobx和recoil。本文先浅谈对状态管理的理解,再梳理和比较上述三种最主流的状态管理方案的使用方法和利弊,并着重介绍新时代原子化状态管理Recoil的使用利弊。

背景知识:状态与状态管理

什么是状态?

先说说状态是什么,前端接触最多的表单状态和UI状态为例,一个表单可以有加载、提交、校验等多个状态,表单里的内容也有多种状态,如单选框(选中和未选中)、输入框(是否选中/是否有内容)等等,这些状态都是渲染和交互必不可少的因素,所以可以简单的把状态理解为我们渲染UI和处理交互的必要工具。

为何需要状态管理?

对于一个较复杂的React项目来说,整个页面需要各种状态来维持数据的交互和展示。而React基本的状态管理只能管理当前组件内的状态,或者处理比较简单的组件间交互(父子/兄弟),涉及到较复杂的组件间数据交互时,处理逻辑就会变得非常复杂且不便。于是需要状态管理方案这样的工具来实现统一管理,当需要管理的数据量非常庞大的时候,好的状态管理方案就能起到事半功倍的作用。

状态管理的本质

数据共享

优雅且高效地在组件之间共享数据永远是各状态管理方案追求的不变目标。全局还是局部:实现数据共享最简单粗暴的方法就是实现全局共享,这也是Redux提供的方案,Redux的广泛应用让使用者们逐渐形成一种共识:React的状态分为两种,一种是Redux管理的全局状态,一种是组件内部使用的局部状态(useState)。但实际我认为与局部状态相对立的不应该是全局状态,而是共享状态,全局状态共享只不过是共享状态的一种实现方式罢了。另一种实现共享状态的可靠方式是局部状态共享,比如React自己提供的Context API就是实现了一个组件树,在该组件树上实现状态的共享,而不是在整个项目上共享数据,这种共享方式相对于全局状态共享就有着能自我管理、拥有自己的生命周期等优势。

逻辑组织

状态管理另一重点是,与数据关联的一系列逻辑如何组织,针对这个问题,Hooks已经提供了比较完整的管理组织方案,如:状态的副作用、性能优化、异步处理等。Hooks的出现让组件内部的逻辑组织得到了极大提升,我们不再需要一味地将状态抽离到全局去组织(如Redux),而是只需要关注当前状态的使用者是谁,是一个还是多个,至于组件内部如何去使用和组织状态,全权交由Hooks去处理就够了。

状态管理方案选择

目前的状态管理具有代表性的方案有:Redux、Mobx和新生代原子化状态管理Recoil,它们各代表不同的数据共享方式;

  1. Redux:FLux式单向数据流,Single Store清晰易懂;
  2. Mbox:响应式Multi Store,灵活使用,多Store相互隔离;
  3. Recoil:原子式单向小回路,使用简单、拓展性高;

Redux

Redux是目前使用最多的状态管理库,其设计初衷是为了解决JaveSript中单页面应用状态管理越发复杂的问题。Redux的使用有三大原则:单一数据源、state只读、纯函数执行修改。

  • 单一数据源:指的是项目的全局状态State都存储在单个Store内的对象树中,提供读写操作;
  • State只读:Store内的状态不可直接修改,State唯一的修改方式,是发送一个Action,触发对应的修改操作;
  • 纯函数执行:Action触发修改依靠的是Reducer纯函数,Reducer会接收State和对应Action,返回修改后的新State;

核心思想

Redux的核心操作就是对全局状态State的读和写。下面展示了Redux 的完整数据流,我们可以直接从全局State中读取渲染UI需要的状态,而UI上又会触发多种事件(Click等)用于修改全局State,这是根本的读写逻辑。而修改State需要几个层层处理,修改请求首先是被Dispatch捕捉到并进行传递,每个修改的操作都会传递一个初始State和修改的操作Action。而传递的下一层就是Reducer,Reducer是执行修改的唯一执行者,其内部根据接收到的初始state和修改操作Action,返回修改后的State,并将最新的State跟新到全局。这就是完整的核心逻辑。

而中间件Middleware的加入将与状态相关的异步操作进一步抽离,在Reducer处理之前,先完成Action中的异步操作(如API请求),这样做使得处理逻辑更加清晰。

优缺点

优点:

  • 全局状态管理、单向数据流清晰严格;
  • 代码风格统一规范,适合团队开发

缺点:

  • State过多十分臃肿,可能导致性能问题;适用:中、大型复杂工程的数据流管理;

Mobx

Mobx的目的是实现简单、可扩展的状态管理,它通过透明的函数响应式编程实现这一点。跟Redux一样,Mobx也是通过Action修改State,再将修改映射到视觉Views上,不同之处在于:

  1. 响应式双向数据绑定,而 Redux 是 Flux 流派的单向数据流。
  2. 观察者模式,当状态改变时,所有衍生都会进行原子级的自动更新。因此永远不可能观察到中间值。
  3. Mobx是多Store且可读写,而Redux 是唯一中心Store且只支持读操作

核心思想

具体来说,Mobx的核心由三个要点组成:

  1. State(状态):多个store数据源,mobx也支持单向数据流传递;
  2. Derivations(衍生):任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生。

     Mobx 有两种形式的衍生:Computed Values(计算值)— 可以使用纯函数(pure function)从当前可观察状态中衍生出的值;Reactions(反应) - Reactions 是当状态改变时需要自动发生的副作用,也是使用较多的衍生;
  3. Actions(动作):Actions是唯一可以修改State的入口;

总体来看,Mobx这种去中心化的Store和双向数据流的设计能让状态管理更加灵活多变、适用于多种场景,但是这种灵活也给实际开发带来了挑战,使用者需要关注每部分Store的处理逻辑,可能陷入混乱。另外,虽然Mobx在中小型项目上表现优秀,面对大型复杂应用场景,Mobx的使用就必须附带严格的规范。

优缺点

优点:

  • 上手简单、拓展性强、使用灵活性能上也有一定优势
  • 多Store源相互独立,不需要关注副作用影响
  • 代码量少,没有像Redux样板代码的束缚

缺点:

  • 过于自由,灵活使用的背后是缺少规范代码,导致团队代码风格不统一
  • 因为没法规范统一,不适用于大型复杂项目适用:
  • 灵活场景的中小型项目

    Recoil

    github地址:https://github.com/facebookex...官网:https://recoiljs.org/zh-hans

    Recoil简介

    Recoil是Facebook开发的状态管理库,用于React项目的状态管理。Recoil定义了一个有向图 ,正交同时又天然连结于你的 React 树上。状态的变化从该图的顶点(我们称之为 atom)开始,流经纯函数 (我们称之为 selector) 再传入组件。具体来说,Recoil 会为使用者创建一个数据流向图,从 atom(共享状态)到 selector(纯函数),再流向 React 组件。所以核心就两个部分:Atom 是组件可以订阅的 state 单位,Selector 可以同步或异步改变此 state。

特点:

原子状态管理模式
按需渲染
读写分离

代码实现

安装Recoil

npm install recoil  
// or
yarn add recoil

RecoilRoot初始化:一般将RecoilRoot放置在父组件的根组件位置,代表其内部的组件使用Recoil数据流。这样我们可以在CharacterCounter组件中直接使用Recoil。

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

Atom:Recoil中用原子atom表示状态,一个atom代表一个状态,并可以在任意组件直接读写。任何atom状态的跟新,也会导致所有使用该atom的组件重新渲染。

eg.下面是一个文本数据,key对应该state唯一表示,default是默认值,当调用没赋值时使用。

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

Selector:Reocil中可以用Selector表示派生状态,也就是状态的转换。可以将派生状态理解为将初始状态传递给一个处理状态纯函数得到的输出。Selector的强大之处在于,它能让我们创建一个依赖于其他数据变化的动态数据,比如说:我们想要得到一个筛选过后的todo List,那我们就可以在初始todo List基础上绑定一个专门筛选的Selector来获得筛选过后的动态列表。

eg.下面是给一个初始atom列表绑定 filter Selector的操作,我们直接使用filteredTodoListState 就能得到筛选后数据:

const todoListFilterState = atom({
  key: 'TodoListFilter',
  default: 'Show All',
});

const filteredTodoListState = selector({
  key: 'FilteredTodoList',
  get: ({get}) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

使用心得

个人也使用过Recoil参与项目的开发,个人体验上,Recoil还是有很多优点的,上手简单、读写逻辑清晰、渲染高效、拓展性强等。但相对的,在使用过程中也暴露了一定的问题,首先就是使用API调用过多,一个state从申明到使用要经过六七个API,API总数量达到了19个,并且每个atom中state必须定义了key和default才能使用,这无疑是繁琐的。

另外,在使用的过程中还遇到一个问题,那就是selector里面的异步请求干扰到了页面元素的默认行为,具体来说我在Selecor里定义了请求数据的接口,在页面渲染时就请求,但同时我在一个组件中放入一个Input框,加上autofocus属性。由于Recoil的处理机制,异步请求的处理影响Input导致其无法自动选中。解决方案是让selector内的请求获得结果后再渲染其他内容。所以Recoil高效的渲染性能背后还隐藏着一些隐患,必须经过时间校验。

优缺点

优点:

  • 简洁、优雅、可拓展
  • 避免无效渲染,高效
  • 细粒度的状态拆分也能给开发者带来更易维护的编码风格;

缺点:

  • 繁琐的API调用
  • selector缺少对副作用的处理
  • 未经时间校验的使用隐患

适用:

  • 皆可适用

总结

整体来看,目前Redux、Mobx和Recoil都有一定的适用场景和使用利弊,但是Mobx还是更多应用于灵活场景的中小型项目。对于复杂的大型项目,之前绝大多数的选择是Redux,因为其较好的开发模版和规范。而现在也有更多的开发者愿意去尝试使用Recoil来开发项目,因为Recoil跟React的使用非常的契合,也拥有诸如渲染性能、拓展性等各方面的优势,少有不足的地方我相信也会在未来被不断的补足。