React组件设计实践总结05 - 状态管理

今天是 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)这个表达式更完全
  • 约束状态的变动。Redux 要求经过dispatch+reducer, mobx 要求数据变动函数使用action装饰或放在flow函数中,目的就是让状态的变动根据可预测性
  • 单向数据流。数据流老是按照 Store -> View -> Store 这样的方式流动, 简化数据流

可是, React 的状态管理方案太多了,选择这些方案可能会让人抓狂,你须要权衡不少东西:shell

  • 面向对象仍是函数式仍是函数响应式?
  • 单 Store 仍是多 Store?
  • 不可变数据仍是可变数据?
  • 写代码爽仍是后期维护爽?
  • 自由仍是约束?
  • 强类型仍是弱类型?
  • 范式化数据仍是非范式化?
  • React 原生仍是第三方?
  • ...



你不须要状态管理

对于大部分简单的应用和中后台项目来讲是不须要状态管理的。说实话这些应用和传统 web 页面没什么区别, 每一个页面都各自独立,每次打开一个新页面时拉取最新数据,增删改查仅此而已. 对于这些场景 React 的组件状态就能够知足, 没有必要为了状态管理而状态管理. 这种各自独立的‘静态’页面,引入状态管理就是过分设计了。

在考虑引入状态管理以前考虑一下这些手段是否能够解决你的问题:

  • 是否能够经过抬升 State 来实现组件间通讯?
  • 若是跨越的层级太多,数据是否能够经过 Context API 来实现共享?
  • 一些全局状态是否能够放在 localStorage 或 sessionStorage 中?
  • 数据是否能够经过外置的事件订阅器进行共享?
  • ...



你不须要复杂的状态管理

当你的应用有如下场景时,就要开始考虑状态管理:

  • 组件之间须要状态共享。同一份数据须要响应到多个视图,且被多个视图进行变动
  • 须要维护全局状态,并在他们变更时响应到视图
  • 数据流变得复杂,React 组件自己已经没法驾驭。例如跨页面的用户协做
  • 须要统一管理应用的状态。好比实现持久化,可恢复,可撤销/重作
  • ...

首先肯定是否须要 Redux、Mobx 这些复杂的状态管理工具? 在 2019 他们不少功能均可以被 React 自己提供的特性取代. 随着 React 16.3 发布了新的 Context API,咱们能够方便地在它之上作简单的状态管理, 咱们应该优先选择这些原生态的状态管理方式

例如: 简单的使用 Context API 来作状态管理:

最近 hooks 用得比较爽(参考上一篇文章: 组件的思惟),我就想配合 Context API 作一个状态管理器, 后来发现早就有人这么干了: unstated-next, 代码只有 38 行(Hooks+Context),接口很是简单:

依赖于 hooks 自己灵活的特性,咱们能够用它来作不少东西, 仅限于想象力. 例如异步数据获取:

抑或者实现 Redux 的核心功能:

Edit redux hooks


总结一下使用 hooks 做为状态管理器的优势:

  • 极简。如上
  • 可组合性. hooks 只是普通函数, 能够组合其余 hooks,以及其余Hooks Container. 上一篇文章提到 hooks 写着写着很像组件,组件写着写着很像 hooks,在用法上组件能够认为是一种'特殊'的 hooks。相比组件, hooks 有更灵活的组合特性
  • 以 react 之名. 基于 Context 实现组件状态共享,基于 hooks 实现状态管理, 这个方式足够通用.
  • hooks 不少灵活的特性足以取代相似 Mobx 这些框架的大部分功能
  • 只是普通的 React 组件,能够在 React inspector 上调试
  • 强类型
  • 基于Context API更容易实现模块化(或者分形)

须要注意的地方

  • 没有外置的状态. 状态在组件内部,没有方法从外部触发状态变动
  • 缺乏约束. 是好处也是坏处, 对于团队和初学者来讲没有约束会致使风格不统一,没法控制项目熵增。好处是能够自定义本身的约束
  • 性能优化. 须要考虑 Context 变动带来的性能问题
  • 调试体验不如 Redux
  • 没有数据镜像, 不能实现诸如事件管理的需求
  • 没有 Redux 丰富的生态

因此 Context+ Hooks 能够用于知足简单的状态管理需求, 对于复杂的状态管理需求仍是须要用上 Redux、Mobx 这类专业的状态管理器.


其余相似的方案

扩展



Redux

unstated 是一个极简的状态管理方案,其做者也说了不要认为unstated 是一个 Redux killer, 不要在它之上构建复杂的工具,也就是不要重复造轮子。因此通常到了这个地步, 其实你就应该考虑 Redux、Mobx、Rxjs 这些复杂的状态管理框架了。

Redux 是学习 React 绕不过的一个框架. 尽管 Redux 的代码只有一百多行,概念却不少,学习曲线很是陡峭,看官方文档就知道了。即便它的实现很简洁,可是开发代码并不简洁(和 mobx 相反, 脏活留给开发者),尤为遵循它的'最佳实践',是从头开始构建一个项目是很是繁琐的. 还在如今有相似 dva 或 rematch 这样的二次封装库来简化它.

本文不打算深刻介绍 Redux 的相关实践, 社区上面有很是多的教程,官方文档也很是详尽. 这里会介绍 Redux 的主要架构和核心思想,以及它的适用场景.

Redux 的主要结构如上,在此以前你先要搞清楚 Redux 的初衷是什么,才能明白它为何要这么设计. 在我看来 Redux 主要为了解决如下两个问题:

  1. 可预测状态
  2. 简化应用数据流

其实这也是 flux 的初衷, 只是有些它有些东西没作好. 明白 Redux 的初衷,如今来看看它的设计就会清晰不少

  • 单一数据源 -> 可预测,简化数据流:数据只能在一个地方被修改

    • 能够简化应用数据流. 解决传统多 model 模型数据流混乱问题(好比一个 model 能够修改其余 model,一个 view 受到多个 model 驱动),让数据变更变得可预测可调试

    • 同构化应用开发

    • 方便调试

    • 方便作数据镜像. 能够实现撤销/重作、时间旅行、热重载、状态持久化和恢复

  • 单向数据流 -> 简化数据流, 可预测

  • 不能直接修改状态 -> 可预测

    • 只能经过 dispatch action 来触发状态变动. action 只是一个简单的对象, 携带事件的类型和 payload
    • reducer 接收 action 和旧的 state, 规约生成新的 state. reducer 只是一个纯函数,能够嵌套组合子 reducer 对复杂 state 树进行规约
    • 不可变数据.
    • 可测试.
  • 范式化和反范式化. Store 只存储范式化的数据,减小数据冗余。视图须要的数据经过 reselect 等手段反范式化

  • 经过中间件隔离反作用 -> 可预测 能够说 Redux 的核心概念就是 reducer,然而这是一个纯函数。为了实现复杂的反作用,redux 提供了相似 Koa 的中间件机制,实现各类反作用. 好比异步请求. 除此以外,能够利用中间件机制,实现通用的业务模式, 减小代码重复。

  • Devtool -> 可预测。经过开发者工具能够可视化数据流


何时应该使用 Redux?

首先仍是警告一下: You Might Not Need Redux, Redux 不是你的第一选择。

当咱们须要处理复杂的应用状态,且 React 自己没法知足你时. 好比:

  • 你须要持久化应用状态, 这样你能够从本地存储或服务器返回数据中恢复应用
  • 须要实现撤销重作这些功能
  • 实现跨页面的用户协做
  • 应用状态很复杂时
  • 数据流比较复杂时
  • 许多不相关的组件须要共享和更新状态
  • 外置状态
  • ...

最佳实践

我的以为react-boilerplate是最符合官方‘最佳实践’的项目模板. 它的应用工做流以下:

特性:

  1. 整合了 Redux 生态比较流行的方案: immer(不可变数据变动),redux-saga(异步数据流处理),reselect(选取和映射 state,支持 memo,可复合),connected-react-router(绑定 react-router v4)
  2. 根据页面分割 saga 和 reducer。见下面 👇 和目录结构
  3. 按需加载 saga 和 reducer(经过 replaceReducer)
  4. 划分容器组件和展现组件

再看看 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 的开发体验:

    • 使用 Ducks 风格组织代码.聚合分散的 reducer,saga,actions...
    • 更简化的 API
    • 提供了简单易用的模块化(或者称为‘分形’)或者命名空间机制。模块自己支持‘状态隔离’,让模块的 reducer、saga 只专一于模块本身的状态. 另外模块还考虑动态加载
    • 内置反作用处理机制。如使用 saga 或 redux-promise
    • 简化了不可变数据的操做方式。 如使用 immer
    • 简化 reducer。Redux 内置了 combineReducers 来复合多个 reducer,在 reducer 内部咱们通常使用 switch 语句来接收 action 和处理数据变更, 其实写起来很是啰嗦. Vuex 和这些封装框架不约而同使用了 key/value 形式, 更为简洁明了
    • 简化 view 层的 connect 接口。如简化 mapProps,mapDispatch 这些代码写起来也比较繁琐

  • 三,强制不可变数据。前面文章也提到过 setState 很啰嗦,为了保证状态的不可变性最简单的方式是使用对象展开或者数组展开操做符, 再复杂点能够上 Immutable.js, 这须要一点学习成本. 好在如今有 immer,能够按照 Javascript 的对象操做习惯来实现不可变数据


  • 四,状态设计

    数据类型通常分为领域数据(Domain data)应用数据(或者称为 UI 数据). 在使用 Redux 时常常须要考虑状态要放在组件局部,仍是全部状态都抽取到 Redux Store?把这些数据放到 Redux Store 里面处理起来好像更麻烦?既然都使用 Redux 了,不把数据抽取到 Redux Store 是否不符合最佳实践? 笔者也时常有这样的困惑, 你也是最佳实践的受害者?

    我以为能够从下面几个点进行考虑:

    • 领域数据仍是应用数据? 领域数据通常推荐放在 ReduxStore 中,咱们一般会将 Redux 的 Store 看做一个数据库,存放范式化的数据。
    • 状态是否会被多个组件或者跨页面共享? Redux Store 是一个全局状态存储器,既然使用 Redux 了,有理由让 Redux 来管理跨越多组件的状态
    • 状态是否须要被镜像化? 若是你的应用要作‘时间旅行(撤销/重作)’或者应用持久化,这个状态须要被恢复,那么应该放到 Redux Store,集中化管理数据是 Redux 的强项
    • 状态是否须要跨越组件的生命周期? 将状态放在组件局部,就会跟着组件一块儿被销毁。若是但愿状态跨越组件的生命周期,应该放到父组件或者 Redux Store 中. 好比一个模态框编辑的数据在关闭后是否须要保留

    原则是能放在局部的就放在局部. 在局部状态和全局状态中取舍须要一点开发经验.

    另外做为一个集中化的状态管理器,为了状态的可读性(更容易理解)和可操做性(更容易增删查改),在状态结构上面的设计也须要花费一些精力的. 这个数据库结构的设计方法是同样的, 在设计状态以前你须要理清各类领域对象之间的关系, 在数据获取和数据变动操做复杂度/性能之间取得平衡.

    Redux 官方推荐范式化 State,扁平化结构树, 减小嵌套,减小数据冗余. 也就是倾向于更方便被更新和存储,至于视图须要什么则交由 reselect 这些库进行计算映射和组合.

    因此说 Redux 没那么简单, 固然 80%的 Web 应用也不须要这么复杂.


  • 五,不方便 Typescript 类型化。无论是 redux 仍是二次封装框架都不是特别方便 Typescript 进行类型推导,尤为是在加入各类扩展后。你可能须要显式注解不少数据类型

    扩展: react-redux-typescript-guide, rematch & Typescript

  • 六,不是分形(Fractal)

    在没有看到@杨剑锋的这条知乎回答以前我也不知道什么叫分形, 我只能尝试解释一下我对分形的理解:

    前面文章也提到过‘分离逻辑和视图’和‘分离容器组件和展现组件’,这两个规则都来自于 Redux 的最佳实践。Redux 就是一个'非分形的架构',以下图,在这种简单的‘横向分层'下, 视图和逻辑(或状态)能够被单独复用,但在 Redux 中却很难将两者做为一个总体的组件来复用:

    集中化的 Store,再经过 Connect 机制可让状态在整个应用范围内被复用;Dumb 组件抽离的状态和行为,也容易被复用

    如今假设你须要将单个 container 抽离成独立的应用,单个 container 是没法独立工做的。在分形的架构下,一个‘应用’有更小的‘应用’组成,‘应用’内部有本身的状态机制,单个应用能够独立工做,也能够做为子应用. 例如 Redux 的鼻祖 Elm 的架构:

    Store的结构和应用的结构保持一致, 每一个 Elm 组件也是一个 Elm 应用,包含完整的Action、Update、Model和View. 使得单独的应用能够被复用

    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

Mobx 提供了一个相似 Vue 的响应式系统,相对 Redux 来讲 Mobx 的架构更容易理解。 拿官方的图来看:

  • 响应式数据. 首先使用@observable 将数据转换为‘响应式数据’,相似于 Vue 的 data。这些数据在一些上下文(例如 computed,observer 的包装的 React 组件,reaction)中被访问时能够被收集依赖,当这些数据变更时相关的依赖就会被通知.

    响应式数据带来的两个优势是 ① 简化数据操做方式(相比 redux 和 setState); ② 精确的数据绑定,只有数据真正变更时,视图才须要渲染,组件依赖的粒度越小,视图就能够更精细地更新

  • 衍生.

    • 衍生数据。Mobx 也推荐不要在状态中放置冗余或可推导的数据,而是使用 @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/下. 在代码层面领域对象有如下特色:

  • 定义了一些字段(@observable)和一些领域对象的操做方法(@action), 可能还关联其余领域对象,好比订单会关联用户和产品
  • 由 Store 来管理生命周期,或者说 Store 就 Model 的容器, 至关于数据库. Store 一般也是单例

示例

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 的组织方式主要有如下优势:

  • 好理解, 容易入手. 经典的 MVC 模式、面向对象,咱们再熟悉不过了. 尤为是熟悉 Java 这些传统面向对象编程范式的后端开发人员. 上文提到的跨团队的项目,咱们选择的就是 mobx 做为状态管理器,对于他们来讲这是最好理解的方式. 可是对于领域对象和领域 Store 的拆分和设计须要一点经验
  • 强类型
  • 代码简洁。相对 Redux 多余的模板代码而言
  • 数据封装性。使用类表达的数据结构能够封装相应的行为

问题

  • 在多个 Store 之间共享数据比较麻烦. 咱们的作法是让全部 Store 都继承一个父类做为中间者,经过事件订阅模式在多个 Store 间进行数据通讯
  • 缺少组织。相对 Redux 而言, 状态过于零散,不加以约束,状态能够被随意修改。咱们不少代码就是这样,懒得写 action,甚至直接在视图层给状态赋值. 因此必定要开始严格模式
  • 没有 Magic. 这是一把双刃剑, Redux 有中间件机制,能够扩展抽象许多重复的工做, 好比为异步方法添加 loading 状态, 可是对 Typescript 不友好; 基于类的方案,无处下手,代码会比较啰嗦, 但更直观
  • 无数据快照,没法实现时间回溯,这是 Redux 的强项,但大部分的应用不须要这个功能; 另外能够经过 mobx-state-tree 实现
  • 没法 hot-reload

还有一些 mobx 自己的问题, 这些问题在上一篇文章也提过, 另外能够看这篇文章(Mvvm 前端数据流框架精讲):

  • 组件侵入性. 须要改变 React 组件本来的结构, 例如全部须要响应数据变更的组件都须要使用 observer 装饰. 组件本地状态也须要 observable 装饰, 以及数据操做方式等等. 对 mobx 耦合较深, 往后切换框架或重构的成本很高

  • 兼容性. mobx v5 后使用 Proxy 进行重构, 但 Proxy 在 Chrome49 以后才支持. 若是要兼容旧版浏览器则只能使用 v4, v4 有一些, 这些坑对于不了解 mobx 的新手很难发现:

    • Observable 数组并不是真正的数组. 好比 antd 的 Table 组件就不认 mobx 的数组, 须要传入到组件之间使用 slice 进行转换
    • 向一个已存在的 observable 对象中添加属性不会被自动捕获

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 纪要的结论:

  • 须要快速开发简单应用的小团队能够考虑使用 MobX,由于 MobX 须要开发的代码量小,学习成本低,上手快,适合于实时系统,仪表盘,文本编辑器,演示软件,但不适用于基于事件的系统
  • Redux 适用于大团队开发复杂应用,Redux 在可扩展性和可维护性方面能够 hold 住多人协做与业务需求多变,适合商业系统、基于事件的系统以及涉及复杂反应的游戏场景。

上述结论的主要依据是 Redux 对 action / event 做出反应,而 MobX 对 state 变化做出反应。好比当一个数据变动涉及到 Mobx 的多个 Store,能够体现出 Redux 的方式更加优雅,数据流更加清晰. 前面都详尽阐述了 Mobx 和 Redux 的优缺点,mobx 还有 MST 加持, 相信读者内心早已有本身的喜爱


扩展

RxJS

若是上文提到的状态管理工具都没法知足你的须要,你的项目复杂程度可能超过全国 99%的项目了. RxJS 可能能够助你一臂之力, RxJS 很是适合复杂异步事件流的应用,笔者在这方面实践也比较少,推荐看看徐飞的相关文章, 另外 Redux(Redux-Observable)和 Mobx 实际上也能够配合 RxJS 使用




其余状态管理方案

推荐这篇文章State of React State Management for 2019




扩展阅读

相关文章
相关标签/搜索