今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案。html
前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 多是文章篇幅太长了?掘金值过低了? 仍是错别字太多了? 后面静下心来想一想,写做对我来讲是一种学习和积累的过程, 让我学习更全面更系统性去描述一个事物. 可是写做确实是一件很是耗时的事情, 文章的每句话都要细细推敲, 还要避免主观性太强避免误导了别人.前端
因此模仿<<内核恐慌>>的口号: "想看的人看,不想看的人就别看"vue
系列目录node
文章目录react
如今的前端框架,包括 React 的一个核心思想就是数据驱动视图, 即UI = f(state)
. 这种开发方式的变化其实得益于 Virtual-DOM, 它使得咱们不须要关心浏览器底层 DOM 的操做细节,只需关心‘状态(state)’和‘状态到 UI 的映射关系(f)’. 因此若是你是初学者,不能理解什么是‘数据驱动’, 仍是不推荐继续阅读文章下面的内容。git
可是随着 state
的复杂化, 框架现有的组件化方式很难驾驭 f
(视图的映射关系变得复杂, 难以被表达和维护); 或者相关类型的应用数据流原本就比较复杂, 组件之间的交互关系多样,原本难以使用UI = f(state)
这种关系来表达; 或者应用的组件状态过于离散,须要统一的治理等等. 咱们就有了状态管理的需求.github
状态管理最基础的解决方式就是分层,也就是说和传统的 MV*
模式没有本质区别, 主流状态管理的主要结构基本都是这样的:web
他们基本都包含这些特色:typescript
f
映射关系, 让UI = f(state)
这个表达式更完全dispatch+reducer
, mobx 要求数据变动函数使用action
装饰或放在flow
函数中,目的就是让状态的变动根据可预测性可是, React 的状态管理方案太多了,选择这些方案可能会让人抓狂,你须要权衡不少东西:shell
对于大部分简单的应用和中后台项目来讲是不须要状态管理的。说实话这些应用和传统 web 页面没什么区别, 每一个页面都各自独立,每次打开一个新页面时拉取最新数据,增删改查仅此而已. 对于这些场景 React 的组件状态就能够知足, 没有必要为了状态管理而状态管理. 这种各自独立的‘静态’页面,引入状态管理就是过分设计了。
在考虑引入状态管理以前考虑一下这些手段是否能够解决你的问题:
当你的应用有如下场景时,就要开始考虑状态管理:
首先肯定是否须要 Redux、Mobx 这些复杂的状态管理工具? 在 2019 他们不少功能均可以被 React 自己提供的特性取代. 随着 React 16.3 发布了新的 Context API,咱们能够方便地在它之上作简单的状态管理, 咱们应该优先选择这些原生态的状态管理方式。
例如: 简单的使用 Context API 来作状态管理:
最近 hooks 用得比较爽(参考上一篇文章: 组件的思惟),我就想配合 Context API 作一个状态管理器, 后来发现早就有人这么干了: unstated-next, 代码只有 38 行(Hooks+Context),接口很是简单:
依赖于 hooks 自己灵活的特性,咱们能够用它来作不少东西, 仅限于想象力. 例如异步数据获取:
抑或者实现 Redux 的核心功能:
总结一下使用 hooks 做为状态管理器的优势:
Hooks Container
. 上一篇文章提到 hooks 写着写着很像组件,组件写着写着很像 hooks,在用法上组件能够认为是一种'特殊'的 hooks。相比组件, hooks 有更灵活的组合特性须要注意的地方
因此 Context+ Hooks 能够用于知足简单的状态管理需求, 对于复杂的状态管理需求仍是须要用上 Redux、Mobx 这类专业的状态管理器.
其余相似的方案
扩展
unstated
是一个极简的状态管理方案,其做者也说了不要认为unstated 是一个 Redux killer, 不要在它之上构建复杂的工具,也就是不要重复造轮子。因此通常到了这个地步, 其实你就应该考虑 Redux、Mobx、Rxjs 这些复杂的状态管理框架了。
Redux 是学习 React 绕不过的一个框架. 尽管 Redux 的代码只有一百多行,概念却不少,学习曲线很是陡峭,看官方文档就知道了。即便它的实现很简洁,可是开发代码并不简洁(和 mobx 相反, 脏活留给开发者),尤为遵循它的'最佳实践',是从头开始构建一个项目是很是繁琐的. 还在如今有相似 dva 或 rematch 这样的二次封装库来简化它.
本文不打算深刻介绍 Redux 的相关实践, 社区上面有很是多的教程,官方文档也很是详尽. 这里会介绍 Redux 的主要架构和核心思想,以及它的适用场景.
Redux 的主要结构如上,在此以前你先要搞清楚 Redux 的初衷是什么,才能明白它为何要这么设计. 在我看来 Redux 主要为了解决如下两个问题:
其实这也是 flux
的初衷, 只是有些它有些东西没作好. 明白 Redux 的初衷,如今来看看它的设计就会清晰不少
单一数据源 -> 可预测,简化数据流:数据只能在一个地方被修改
能够简化应用数据流. 解决传统多 model 模型数据流混乱问题(好比一个 model 能够修改其余 model,一个 view 受到多个 model 驱动),让数据变更变得可预测可调试
同构化应用开发
方便调试
方便作数据镜像. 能够实现撤销/重作、时间旅行、热重载、状态持久化和恢复
单向数据流 -> 简化数据流, 可预测
不能直接修改状态 -> 可预测
范式化和反范式化. Store 只存储范式化的数据,减小数据冗余。视图须要的数据经过 reselect 等手段反范式化
经过中间件隔离反作用 -> 可预测 能够说 Redux 的核心概念就是 reducer,然而这是一个纯函数。为了实现复杂的反作用,redux 提供了相似 Koa 的中间件机制,实现各类反作用. 好比异步请求. 除此以外,能够利用中间件机制,实现通用的业务模式, 减小代码重复。
Devtool -> 可预测。经过开发者工具能够可视化数据流
何时应该使用 Redux?
首先仍是警告一下: You Might Not Need Redux, Redux 不是你的第一选择。
当咱们须要处理复杂的应用状态,且 React 自己没法知足你时. 好比:
最佳实践
我的以为react-boilerplate是最符合官方‘最佳实践’的项目模板. 它的应用工做流以下:
特性:
immer
(不可变数据变动),redux-saga
(异步数据流处理),reselect
(选取和映射 state,支持 memo,可复合),connected-react-router
(绑定 react-router v4)再看看 react-boilerplate 目录结构. 这是我我的比较喜欢的项目组件方式,组织很是清晰,颇有参考意义
/src
/components # 展现组件
/containers # 🔴容器/页面组件
/App # 根组件, 例如放置Provider和Router
/HomePage # 页面组件
index.js # 页面入口
constants.js # 🔴 在这里定义各类常量。包括Action Type
actions.js # 🔴 定义各类Action函数
saga.js # 🔴 redux-saga 定义各类saga方法, 用于处理异步流程
reducer.js # 🔴 reducer。 页面组件的reducer和saga都会按需注入到根store
selectors.js # 🔴 redux state映射和计算
message.js
Form.js # 各类局部组件
Input.js
...
/FeaturePage # 其余页面组件结构同上
...
/translations # i18n 翻译文件
/utils
reducerInjectors.js # 🔴reducer 注入器, 实现和页面组件一块儿按需注入
sagaInjectors.js # 🔴saga 注入器, 同上
lodable.js
app.js # 应用入口
i18n.js # i18n配置
configureStore.js # 🔴 建立和配置Redux Store
reducers.js # 🔴 根reducers, 合并全部'页面状态'和'全局状态'(如router, language, global(例如用户鉴权信息))
复制代码
🤬 开始吐槽!
一,Redux 核心库很小,只提供了 dispatch 和 reducer 机制,对于各类复杂的反作用处理 Redux 经过提供中间件机制外包出去。社区有不少解决方案,redux-promise, redux-saga, redux-observable... 查看 Redux 的生态系统.
Redux 中间件的好处是扩展性很是好, 开发者能够利用中间件抽象重复的业务 e 中间件生态也百花齐放, 可是对于初学者则不友好.
TM 起码还得须要去了解各类各样的库,横向比较的一下才知道本身须要搭配哪一个库吧? 那好吧,就选 redux-saga 吧,star 数比较多。后面又有牛人说不要面向 star 编程,选择适合本身团队的才是最好的... 因此挑选合适的方案以前仍是得要了解各类方案自己吧?。
Vue 之因此学习曲线比较平缓也在于此吧。它帮助咱们作了不少选择,提供简洁的解决方案,另外官方还提供了风格指南和最佳实践. 这些选择适合 80%以上的开发需求. 开发者减小了不少折腾的时间,能够专心写业务. 这才是所谓的‘渐进式’框架吧, 对于不爱折腾的或初学者,咱们帮你选择,但也不会阻碍你往高级的地方走。 这里能够感觉到 React 社区和 Vue 社区的风格彻底不一样.
在出现选择困难症时,仍是看看别人怎么选择,好比比较有影响力的团队或者流行的开源项目(如 dva,rematch),选取一个折中方案, 后续有再慢慢深刻研究. 对于 Redux 目前比较流行的组合就是: immer+saga+reselect
二,太多模板代码。好比上面的 react-boilerplate, 涉及五个文件, 须要定义各类 Action Type、Action、 Reducer、Saga、Select. 因此即使想进行一个小的状态变化也须要更改好几个地方:
笔者我的更喜欢相似 Vuex 这种Ducks
风格的组织方式,将模块下的 action,saga,reducer 和 mapper 都组织在一个文件下面:
Redux 的二次封装框架基本采用相似的风格, 如rematch
这些二次封装框架通常作了如下优化(其实能够当作是 Vuex 的优势),来提高 Redux 的开发体验:
三,强制不可变数据。前面文章也提到过 setState 很啰嗦,为了保证状态的不可变性最简单的方式是使用对象展开或者数组展开操做符, 再复杂点能够上 Immutable.js, 这须要一点学习成本. 好在如今有 immer,能够按照 Javascript 的对象操做习惯来实现不可变数据
四,状态设计。
数据类型通常分为领域数据(Domain data)和应用数据(或者称为 UI 数据). 在使用 Redux 时常常须要考虑状态要放在组件局部,仍是全部状态都抽取到 Redux Store?把这些数据放到 Redux Store 里面处理起来好像更麻烦?既然都使用 Redux 了,不把数据抽取到 Redux Store 是否不符合最佳实践? 笔者也时常有这样的困惑, 你也是最佳实践的受害者?
我以为能够从下面几个点进行考虑:
原则是能放在局部的就放在局部. 在局部状态和全局状态中取舍须要一点开发经验.
另外做为一个集中化的状态管理器,为了状态的可读性(更容易理解)和可操做性(更容易增删查改),在状态结构上面的设计也须要花费一些精力的. 这个数据库结构的设计方法是同样的, 在设计状态以前你须要理清各类领域对象之间的关系, 在数据获取和数据变动操做复杂度/性能之间取得平衡.
Redux 官方推荐范式化 State,扁平化结构树, 减小嵌套,减小数据冗余. 也就是倾向于更方便被更新和存储,至于视图须要什么则交由 reselect 这些库进行计算映射和组合.
因此说 Redux 没那么简单, 固然 80%的 Web 应用也不须要这么复杂.
五,不方便 Typescript 类型化。无论是 redux 仍是二次封装框架都不是特别方便 Typescript 进行类型推导,尤为是在加入各类扩展后。你可能须要显式注解不少数据类型
六,不是分形(Fractal)
在没有看到@杨剑锋的这条知乎回答以前我也不知道什么叫分形, 我只能尝试解释一下我对分形的理解:
前面文章也提到过‘分离逻辑和视图’和‘分离容器组件和展现组件’,这两个规则都来自于 Redux 的最佳实践。Redux 就是一个'非分形的架构',以下图,在这种简单的‘横向分层'下, 视图和逻辑(或状态)能够被单独复用,但在 Redux 中却很难将两者做为一个总体的组件来复用:
如今假设你须要将单个 container 抽离成独立的应用,单个 container 是没法独立工做的。在分形的架构下,一个‘应用’有更小的‘应用’组成,‘应用’内部有本身的状态机制,单个应用能够独立工做,也能够做为子应用. 例如 Redux 的鼻祖 Elm 的架构:
Redux 不是分形和 Redux 自己的定位有关,它是一个纯粹的状态管理器,不涉及组件的视图实现,因此没法像 elm 和 cyclejs 同样造成一个完整的应用闭环。 其实能够发现 react 组件自己就是分形的,组件本来就是状态和视图的集合.
分形的好处就是能够实现更灵活的复用和组合,减小胶水代码。显然如今支持纯分形架构的框架并不流行,缘由多是门槛比较高。我的认为不支持分形在工程上还不至于成为 Redux 的痛点,咱们能够经过‘模块化’将 Redux 拆分为多个模块,在多个 Container 中进行独立维护,从某种程度上是否就是分形?另外这种横向隔离的 UI 和状态,也是有好处的,好比 UI 相比业务的状态变化的频度会更大.
我的感受到页面这个级别的分化刚恰好,好比方便分工。好比最近笔者就有这样一个项目, 咱们须要将一个原生 Windows 客户端转换成 electron 实现,限于资源问题,这个项目涉及到两个团队之间协做. 对于这个项目应用 Store 就是一个接口层,Windows 团队负责在这里维护状态和实现业务逻辑,而咱们前端团队则负责展现层. 这样一来 Windows 不须要学习 React 和视图展现,咱们也不须要关系他们复杂的业务逻辑(底层仍是使用 C++, 暴露部分接口给 node)
七,可能还有性能问题
总结
本节主要介绍的 Redux 设计的动机,以及围绕着这个动机一系列设计, 再介绍了 Redux 的一些缺点和最佳实践。Redux 的生态很是繁荣,若是是初学者或不想折腾仍是建议使用 Dva 或 rematch 这类二次封装框架,这些框架一般就是 Redux 一些最佳实践的沉淀, 减小折腾的时间。固然这只是个开始,组织一个大型项目你还有不少要学的。
扩展阅读
Mobx 提供了一个相似 Vue 的响应式系统,相对 Redux 来讲 Mobx 的架构更容易理解。 拿官方的图来看:
响应式数据. 首先使用@observable
将数据转换为‘响应式数据’,相似于 Vue 的 data。这些数据在一些上下文(例如 computed,observer 的包装的 React 组件,reaction)中被访问时能够被收集依赖,当这些数据变更时相关的依赖就会被通知.
响应式数据带来的两个优势是 ① 简化数据操做方式(相比 redux 和 setState); ② 精确的数据绑定,只有数据真正变更时,视图才须要渲染,组件依赖的粒度越小,视图就能够更精细地更新
衍生.
@computed
计算衍生的状态. computed 的概念相似于 Redux 中的 reselect,对范式化的数据进行反范式化或者聚合计算数据变动. mobx 推荐在 action/flow(异步操做)
中对数据进行变动,action 能够认为是 Redux 中的 dispatch+reducer 的合体。在严格模式下 mobx 会限制只能在 action 函数中进行变动,这使得状态的变动可被追溯。推荐在 flow 函数中隔离反作用,这个东西和 Redux-saga 差很少,经过 generator 来进行异步操做和反作用隔离
上面就是 Mobx 的核心概念。举一个简单的例子:
可是Mobx 不是一个框架,它不会像 Redux 同样告诉你如何去组织代码,在哪存储状态或者如何处理事件, 也没有最佳实践。好处是你能够按照本身的喜爱组件项目,好比按照 Redux(Vuex)方式,也可使用面向对象方式组织; 坏处是若是你没有相关经验, 会不知所措,不知道如何组织代码
Mobx 通常使用面向对象的方式对 Store 进行组织, 官方文档构建大型可扩展可维护项目的最佳实践也介绍了这种方式, 这个其实就是经典的 MV* 模式:
src/
components/ # 展现组件
models/ # 🔴 放置一些领域对象
Order.ts
User.ts
Product.ts
...
stores/ # store
AppStore.ts # 应用Store,存放应用全局信息,如auth,language,theme
OrderStore.ts
RootStore.ts # 根Store,组合全部下级Store
...
containers/
App/ # 根组件
Orders/ # 页面组件
...
utils/
store.ts # store初始化
index.tsx
复制代码
领域对象
面向对象领域有太多的名词和概念,并且比较抽象,若是理解有误请纠正. 暂且不去理论领域对象是什么,尚且视做是现实世界中一个业务实体在 OOP 的抽象. 具体来讲能够当作MVC
模式中的 M, 或者是 ORM 中数据库中映射出来的对象.
对于复杂的领域对象,会抽取为单独的类,好比前面例子中的Todo
类, 抽取为类的好处是它具备封装性,能够包含关联的行为、定义和其余对象的关联关系,相比纯对象表达能力更强. 缺点就是很差序列化
由于它们和页面的关联关系较弱,且可能在多个页面中被复用, 因此放在根目录的models/
下. 在代码层面领域对象有如下特色:
示例
import { observable } from 'mobx';
export default class Order {
public id: string;
@observable
public name: string;
@observable
public createdDate: Date;
@observable
public product: Product;
@observable
public user: User;
}
复制代码
Store
Store 只是一个 Model 容器, 负责管理 model 对象的生命周期、定义衍生状态、封装反作用、和后端接口集成等等. Store 通常是单例. 在 Mobx 应用中通常会划分为多个 Store 绑定不一样的页面。
示例
import { observable, computed, reaction } from 'mobx';
export default class OrderStore {
// 定义模型state
@observable orders: Order[] = [];
_unSubscribeOrderChange: Function
rootStore: RootStore
// 定义衍生数据
@computed get finishedOrderCount() {}
@computed get finishedOrders() {}
// 定义反作用衍生
subscribeOrderChange() { this._unSubscribeOrderChange = this.orders.observe((changeData) => {} }
// 定义action
@action addOrder (order) {}
@action removeOrder (order) {}
// 或者一些异步的action
async fetchOrders () {
const orders = await fetchOrders()
orders.forEach(item => this.addOrder(new OrderModel(this, item)))
}
// 初始化,初始化数据结构,初始化订阅等等
initialize () {
this.subscribeOrderChange()
}
// 一些清理工做
release () {
this._unSubscribeOrderChange()
}
constructor(store: RootStore) {
// 和rootStore进行通讯
this.rootStore = store
}
}
复制代码
根 Store
class RootStore {
constructor() {
this.appStore = new AppStore(this);
this.orderStore = new OrderStore(this);
...
}
}
复制代码
<Provider rootStore={new RootStore()}>
<App />
</Provider>
复制代码
看一个 真实世界的例子
这种传统 MVC 的组织方式主要有如下优势:
问题
还有一些 mobx 自己的问题, 这些问题在上一篇文章也提过, 另外能够看这篇文章(Mvvm 前端数据流框架精讲):
组件侵入性. 须要改变 React 组件本来的结构, 例如全部须要响应数据变更的组件都须要使用 observer 装饰. 组件本地状态也须要 observable 装饰, 以及数据操做方式等等. 对 mobx 耦合较深, 往后切换框架或重构的成本很高
兼容性. mobx v5 后使用 Proxy 进行重构, 但 Proxy 在 Chrome49 以后才支持. 若是要兼容旧版浏览器则只能使用 v4, v4 有一些坑, 这些坑对于不了解 mobx 的新手很难发现:
MV*
只是 Mobx 的其中一种主流组织方式, 不少文章在讨论 Redux 和 mobx 时每每会沦为函数式和面向对象之争,而后就下结论说 Redux 更适合大型项目,下这种结论最主要的缘由是 Redux 有更多约束(only one way to do it), 适合项目的演进和团队协做, 而不在于函数式和面向对象。固然函数式和面向对象范式都有本身擅长的领域,例如函数式适合数据处理和复杂数据流抽象,而面向对象适合业务模型的抽象, 因此不要一竿子打死.
换句话说适不适合大型项目是项目组织问题, Mobx 前期并无提出任何解决方案和最佳实践。这不后来其做者也开发了mobx-state-tree这个神器,做为 MobX 官方提供的状态模型构建库,MST 吸取了 Redux 等工具的优势,旨在结合不可变数据/函数式(transactionality, traceability and composition)和可变数据/面向对象(discoverability, co-location and encapsulation)二者的优势, 提供了不少诸如数据镜像(time travel)、hot reload、action middleware、集成 redux-devtools 以及强类型(Typescript + 运行时检查(争议点))等颇有用的特性, 其实它更像是后端 ActiveRecord 这类 ORM 工具, 构建一个对象图。
典型的代码:
限于笔者对 MST 实践很少,并且文章篇幅已经很长,因此就不展开了,后续有机会再分享分享。
仍是得下一个结论, 选择 Mobx 仍是 Redux? 这里仍是引用来自MobX vs Redux: Comparing the Opposing Paradigms - React Conf 2017 纪要的结论:
上述结论的主要依据是 Redux 对 action / event 做出反应,而 MobX 对 state 变化做出反应。好比当一个数据变动涉及到 Mobx 的多个 Store,能够体现出 Redux 的方式更加优雅,数据流更加清晰. 前面都详尽阐述了 Mobx 和 Redux 的优缺点,mobx 还有 MST 加持, 相信读者内心早已有本身的喜爱
扩展
若是上文提到的状态管理工具都没法知足你的须要,你的项目复杂程度可能超过全国 99%的项目了. RxJS 可能能够助你一臂之力, RxJS 很是适合复杂异步事件流的应用,笔者在这方面实践也比较少,推荐看看徐飞的相关文章, 另外 Redux(Redux-Observable)和 Mobx 实际上也能够配合 RxJS 使用
推荐这篇文章State of React State Management for 2019