前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

初识 MobX,从核心 API 到计数器实战

首页2018-08-31 16:25:24Front-End
JavaScriptMobX状态管理

那阵子团队里 Redux 写得有点疲。加一个「购物车商品数量」这么小的功能,要动 actionTypes.js、actions/cart.js、reducers/cart.js 三个文件,再回组件里补 mapStateToProps,实际干活的逻辑就是 state.count += 1。后来看到 MobX 的示例,一个 @observable count = 0 加一个 @observer,页面就跟着变了,当时的第一反应是这也太省事了,第二反应是它凭什么能做到。

这篇是我把 MobX 摸一遍之后的笔记。从它和 Redux 的差别聊起,逐个拆七个核心 API,最后串一个完整的计数器。文章写于 2018 年,用的是当时主流的装饰器写法,我把原样保留了,另外补了几段说明现在 MobX 6 之后该怎么写,两边不混着看。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • MobX 的整体流程,以及它和 Redux 在数据流上的根本差别
  • MobX 的优点和缺点各在哪,什么场景下用它更划算
  • observable / computed / action 三块基石的职责边界
  • observer 怎么让 React 组件自动跟着数据重渲
  • autorun、reaction、flow 分别解决什么问题,怎么选
  • 一个完整可跑的计数器示例,把上面的概念全串起来
  • 装饰器写法在 MobX 6 之后的变化,以及现在的推荐入口

# 一、认识 MobX

# 1.1 先看看 MobX 里有什么

理解一个库最直接的办法是把它打印出来看看导出了哪些东西。

import * as mobx from 'mobx'
console.log(mobx)
@前端进阶之旅: 代码已经复制到剪贴板

打印 mobx 对象查看导出的 API 列表

从这张图能看出来,MobX 的 API 面并不大,核心就是 observable、computed、action、autorun、reaction、when、configure 这些。真正需要理解的概念比 API 数量还少。

再看它的整个流程:

MobX 的数据流转流程图,从 action 到 state 再到 computed 和 reaction

这条链路是 MobX 的骨架。事件触发 action,action 修改 state,state 是 observable 的,它一变就会通知依赖它的 computed 重新计算,再通知 reaction 执行副作用,而 React 组件本身就是最重要的那个 reaction。

注意这里没有「派发到某个中心再分发下去」的环节。谁读了什么,MobX 在运行时就记着,改了就直接通知那几个人。这是它和 Redux 最大的结构性差别。

# 1.2 MobX 和 Redux 的比较

摆开来看有四条差别。

Redux 是单一数据源,而 MobX 往往是多个 store。MobX 可以根据应用的 UI、数据或业务逻辑来组织 store,具体怎么分需要你自己权衡。这个自由度是双刃剑,小项目很爽,大项目没有约定就容易乱。

Redux store 使用普通的 JavaScript 对象结构,MobX 则把常规 JavaScript 对象包裹起来,赋予 observable 的能力,通过隐式订阅自动跟踪 observable 的变化。MobX 是观察引用的,在跟踪函数中(比如 computed value、reactions 这些)任何被引用的 observable 属性都会被记录,一旦引用改变,MobX 就会作出反应。

这里有个坑要注意:不在跟踪函数中的属性不会被跟踪,在异步中访问的属性也不会被跟踪。很多「明明改了数据但组件不更新」的问题,根子都在这条上。

Redux 的 state 是只读的,只能通过把之前的 state 与触发的 action 结合产生新的 state,因此是纯净的(pure)。而 MobX 的 state 既可读又可写,action 是非必须的,可以直接赋值改变,因此是不纯净的(impure)。

Redux 需要你去规范化你的 state。Immutable 数据使得 Reducer 在更新时需要把状态树的祖先数据复制并更新,新的对象会导致与之 connect 的所有 UI 组件都重复渲染。所以 Redux state 不建议做深层嵌套,或者需要在组件里用 shouldComponentUpdate 优化。而 MobX 只自动更新你所关心的那部分,不必担心嵌套带来的重渲染问题。

用一句话概括覆盖范围:redux 管理的是 STORE -> VIEW -> ACTION 的整个闭合流程,而 mobx 只关心 STORE -> VIEW 这一段。

Redux 的三大原则和它为什么要主动给状态修改加约束,我在 Redux 原理与三大原则 里单独写过,对照着看差别会更清楚。

# 1.3 优点

基于运行时的数据订阅。 mobx 的数据依赖始终保持最小,而且是运行时自动追踪的。如果用 redux,可能一不小心就多订阅或者少订阅了数据,多了性能差,少了不更新。为了达到高性能,得借助 PureRenderMixin 以及 reselect 对 selector 做缓存,这是额外的心智负担。

通过 OOP 的方式组织领域模型(domain model)。 OOP 的方式在某些场景下会比较方便,尤其是容易抽取 domain model 的时候。而且 mobx 支持用引用的方式引用数据,很容易形成模型图(model graph)。一个订单对象能直接拿到它的用户对象,而不是拿一个 userId 再去别的 slice 里查。

修改数据方便自然。 mobx 是基于原生的 JavaScript 对象、数组和 Class 实现的,修改数据不需要额外语法成本,也不需要始终返回一个新的数据,直接操作就行。

# 1.4 缺点

缺最佳实践和社区积累。 mobx 出现得比较晚,遇到的问题可能社区都没有遇到过。并且它没有很好的扩展和插件机制。

可以随意修改 store。 redux 里唯一能改数据的地方是 reducer,这保证了应用的可预测性;而 mobx 可以在任何地方修改数据触发更新,给人一种不太安全的感觉。这个问题后来是有解的,mobx 2.2 加入了 action 的支持,开启 strict mode 之后就只有 action 可以对数据进行修改,修改入口重新收敛了。

逻辑层的限制。 如果更新逻辑不能很好地封装在 domain class 里,用 redux 会更合适。另外 mobx 缺少类似 redux-saga 的库,复杂业务流程的编排不知道放哪合适。

这条我自己的感受是最真实的。做一个多步骤的下单流程,Redux 那边有 saga 可以把流程写成一条线,MobX 这边你得自己找地方放。

# 二、核心 API

七个 API 的关系可以先看这张全景图:

MobX 核心 API 的关系图,observable 与 computed、action、reaction 的关联

读法是这样的:observable 是源头,action 是唯一推荐的修改入口,computed 是从源头派生出的值,autorun / reaction / observer 都属于副作用,负责在数据变化时做点什么。

# 2.1 observable

Observable 值可以是 JS 基本数据类型、引用类型、普通对象、类实例、数组和映射。被它修饰的 state 会暴露出来供观察者使用。

// Observable 值可以是JS基本数据类型、引用类型、普通对象、类实例、数组和映射
@observable title = 'this is about page'
@observable num = 0

// 计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值
@computed get getUserInfo(){
   return `我是computed经过计算的getter,current num:${this.num}`
}
// 注意:当你使用装饰器模式时,@action 中的 this 没有绑定在当前这个实例上,要通过 @action.bound 来绑定 使得 this 绑定在实例对象上
@action.bound add(){
    this.num ++
}
@action.bound reduce(){
    this.num --
}
@前端进阶之旅: 代码已经复制到剪贴板

这段把三个基石一次性演示完了,值得逐行看一下。@observable 标记的两个字段是数据源;@computed 修饰的 getter 是派生值,读它的时候才计算;@action.bound 里的 .bound 是把方法绑到实例上,这样你可以直接写 onClick={store.add} 而不用包一层箭头函数。

顺带说一句,这里的 @observable num = 0 是类属性写法,需要 babel 的装饰器插件加类属性插件才能编译,两个都得配,少一个就报语法错误。

# 2.2 observer

observer 可以用作包裹 React 组件的高阶组件。在组件的 render 函数中任何已使用的 observable 发生变化时,组件都会自动重新渲染。注意 observer 是由 mobx-react 包提供的,而不是 mobx 本身。

它的原理,用一句话说就是用 mobx.autorun 包装了组件的 render 函数,确保任何组件渲染中使用的数据变化时都能强制刷新组件。

那为什么有时候数据明明变了组件却没反应呢?

因为「已使用」这三个字是严格的。observer 建立的订阅只覆盖本次 render 期间真正被读取过的字段。在组件外面提前解构好再传进来,读取动作发生在 observer 的追踪范围之外,订阅就没建立起来。

// 失效:props 已经在父组件里被读出来了,这里拿到的是一个死值
const Bad = observer(({ count }) => <span>{count}</span>)

// 正确:在 observer 内部读 store 上的属性
const Good = observer(({ store }) => <span>{store.count}</span>)
@前端进阶之旅: 代码已经复制到剪贴板

我一开始也以为 observer 是给组件装了个全局监听器,想通「读了才订阅」这一点之后,很多诡异问题就自己解释了。

# 2.3 computed

计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值,用于获取由基础 state 衍生出来的值。如果基础值没有变,获取衍生值时就会走缓存,这样就不会引起虚拟 DOM 的重新渲染。

它有两个入口:getter 负责获得计算得到的新 state 并返回;setter 不能用来直接改变计算属性的值,但可以用来做「逆向」衍生。

通过 @computed 加 getter 函数来定义衍生值:

class Foo {
    @observable length = 2;
    @computed get squared() {
        return this.length * this.length;
    }
    set squared(value) { // 这是一个自动的动作,不需要注解
        this.length = Math.sqrt(value);
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

这个 setter 的用法看着有点绕,它的实际场景是「双向换算」。摄氏度和华氏度两个输入框,改哪一个另一个都要跟着变,用 computed 的 setter 写就很自然。

这里有个坑要注意:computed 的缓存只在它被某个 reaction 观察时才生效。如果你在组件外面直接读一个没人观察的 computed,MobX 每次都会重新计算。表现就是「明明加了 computed 却没变快」,在写单元测试或者在事件回调里直接读 store 的时候容易遇到。

# 2.4 action

只有在 action 中,才可以修改 MobX 中 state 的值。当你使用装饰器模式时,@action 中的 this 没有绑定在当前实例上,要通过 @action.bound 来绑定。

不过这条规则默认是不强制的,得靠严格模式打开:

fe
  • 一、认识 MobX
    • 1.1 先看看 MobX 里有什么
    • 1.2 MobX 和 Redux 的比较
    • 1.3 优点
    • 1.4 缺点
  • 二、核心 API
    • 2.1 observable
    • 2.2 observer
    • 2.3 computed
    • 2.4 action
    • 2.5 autorun
    • 2.6 reaction
    • 2.7 flow
  • 三、计数器例子
  • 四、装饰器写法在今天的处境
  • 五、应用案例
  • 总结
  • 参考

← Taro开发小程序体验浅析 redux-saga 中间件及用法,对比 redux-thunk →