使用 React 的过程中,或多或少都会接触到 状态管理 ,从 Flux 到 Redux 到 Dva,各种状态管理工具满天飞,今天让我们来聊一聊这些工具,以及思考在这些工具的基础上如何更简洁的使用 状态管理 这个大杀器。

引子

聊状态管理之前,先让我们梳理下在原生 React 中如何进行组件间通信。

  • Props

    子组件需要父组件的数据,通过 props 一层层向下传递。

    子组件需要更改父组件数据,通过 props callback 调用父组件的方法更新数据。

    多个组件共享数据,抽象 Container 组件,从 Container 组件统一分发 props。

    这样确实可以让应用的数据流非常明确,但是随着应用复杂度提升,组件层级加深,不同组件都要抽象 Container 组件,带来的状态提升问题非常严重,后期通信成本会越来越高。

  • Context

    为了减少 props 带来的嵌套问题,可以使用 Context API。

    在顶级公共组件中,定义 getChildContext 方法,返回子组件所需的数据,子组件通过 this.context 访问相应数据。

    这样就省去了 props 传递带来的复杂度,但是这样也有自己的问题,比如数据过多 getChildContext 方法会非常臃肿,比如 context 跨多层组件获取时,需要上层组件都进行 render,最终组件才可获取数据等。

  • 观察者模式

    观察者模式就是组件通信的一种通用方法了,在需要更新时订阅 event 的变化传入回调函数即可。

    同样的,观察者模式也会带来很多问题,比如订阅散落在各个组件中,维护成本高,违背 React 单向数据流的理念。

    所以,一个统一的状态管理工具从 React 诞生之始,就备受期待。

Flux

Flux 其实更像是一种思想,类似 MVC、MVVM,提倡严格的单向数据流,组件所用数据全部存放在 Store 中,只能通过 Dispatcher 来告知 Store 修改数据,View 层则订阅数据变化来触发页面更新动作。

在这个思想的基础上,不管什么框架都可以基于这个实现自己的状态管理工具。

Flux 的几个主要概念:

  • View:视图层,根据数据展示界面,订阅数据变化来自动更新界面,并响应用户行为触发 Action

  • Store:存放和处理数据相关的逻辑

  • Action:触发 Dispatcher

  • Dispatcher:告知 Store 修改数据

Flux 需要手动在 view 层订阅数据变化手动触发页面更新,而且没有很好的异步解决方案,所以在 Redux 出现以后,迅速被代替。

Redux

Redux 是在吸收 Flux 思想的基础上产生的状态管理工具。

此处默认 Redux 为 React-Redux

Redux 的核心概念:

  • Store:管理 State,并有 dispatch、subscribe 等方法

  • Action:响应用户事件,触发 Reducer

  • Dispatch:分发 Action

  • Reducer:修改 Store 中的数据,返回一个全新的 State

  • Provider:将 store 注入到应用中

  • Connect:将特定的 state 和 dispatch 函数注入到组件中

  • Middleware:类似 Koa 的中间件思想,拓展 Redux 能力,如 log、dev-tool 等

虽说 Redux 吸收了 Flux 的优秀思想,但是依旧存在着众多问题,比如:

  • 约定太多

    Redux 中提出了很多约定,但是根本不知道为啥要这么写,新手看起来非常头大。

    比如 Action 只是一个个 JS 对象,为什么要放在不同的文件里?

    比如 Reducer 强调纯函数,每次修改必须返回全新的 State。

    比如 Reducer 大量 case 仅仅只是为了改变一个值。

    比如应用复杂了之后,类似文件大量重复,组织繁琐。

    当然,这些可以用更好的代码组织方式来避免,不过既然是约定,如果每个人都有自己的组织方式,那约定也就失去了意义。

  • 异步处理

    Redux 中的 Action 其实就是一个个 JS 对象,当需要在 Action 中进行异步操作时,只能在 Redux 上做一些封装。

    目前社区最通用的解决方案是 redux-saga,通过在全局跑着 generator,监听 dispatch 函数分发的 actionType,当命中时,进入 saga,执行异步操作,完成后再调用真实的 Reducer。

    虽说解决了很多问题,但是缺点同样明显。

    比如需要定义的文件又多了一个 saga,应用复杂后,代码组织成本直线增加。

    比如错误处理,基本每个 task 都需要 try catch 捕获错误。

    比如都 9102 年了,依旧需要使用 “丑陋” 的 yield 写法。

    综上,在直接使用 Redux 时,绝大多数都会选择对 Redux 进行一层封装,集合多种中间件的功能。

Dva

说完 Flux 和 Redux ,发现 Dva 才是 “真香” 的解决方案。

dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

以上是 Dva 的官方说明,简单理解,就是可以让 react-redux 和 redux-saga 编写的代码组织起来更合理,维护起来更简单。

比如,原先散落在各处的 Action、saga,可以在同一个对象中书写。

比如,简化配置过程,项目可快速初始化。

比如,集成 redux、redux-saga、redux-router。

比如,简洁的 API,相较于 redux 更容易理解的 app.model、app.router、app.use、app.start API。

比如,动态注册 model。

接下来又是喜闻乐见的转折,Dva 是有很多优点,但是它的缺点同样“致命”。

  • Dva 是一个轻量框架

    Dva 集成了很多你可能并不需要的东西,当你想要部分使用它的功能,有点难。

    比如只能绑定使用 Dva 中的 react-router ,无法独立升级。

    比如打包脚本,想要整合进自己的项目,还需要做一番努力。

  • redux-saga 的缺点

    Dva 集成 redux-saga 来实现异步 action,所以它同样继承了 saga generator 的写法,以及 saga put take 等 API,理解困难,书写不直观。

    说了这么多,最终目的还是要解决在使用 状态管理 时遇到的各种问题,综合对比以上的各种方案,我们简单列一下还未解决的痛点。

待解决的痛点

  • 低入侵

    尽可能减少对原应用的入侵,提供尽量简洁的 API 来实现 Redux 功能

  • 异步 Action

    提供更为现代,更为直观的异步 Action 解决方案

  • 跨组件获取 Store

    子组件可以简便的获取 Stroe 树

  • 模块化

    提供更自然的模块嵌套解决方案,减少心智负担

  • 辅助函数

    提供 mapStateToProps、mapActionsToProps、mapReducersToProps 辅助函数,简化组件状态结构

总结

对比市面上流行的 状态管理 工具,发现并没有完全契合需求的方案,不如我们自己在 Redux 上加一层简单的封装,来解决使用过程中的痛点问题。

下一章,我们来一步步对 “Redux” 进行封装,让 “Redux” 更加简洁,更加强大。