在前端应用开发中,状态管理一直是核心议题。随着应用复杂度不断攀升,如何高效、可维护地管理应用状态成为开发者必须面对的挑战。MobX 作为一款响应式状态管理库,以其简洁的API和优秀的性能表现赢得了众多开发者的青睐。本文将带你深入了解 MobX 的核心概念,并通过实际案例展示如何与 React 协同构建响应式应用。
# 一、为什么选择MobX
# 1.1 传统状态管理的痛点
在 MobX 出现之前,开发者通常使用 Redux 来管理应用状态。Redux 采用单一数据源模式,所有状态存储在一个只读的 store 中,只能通过 action 和 reducer 来更新状态。这种方式虽然逻辑清晰,但在实际应用中存在一些痛点:
状态冗余更新问题:由于 Redux 的状态是不可变的,每次更新都需要创建新的状态对象。这导致与 store 连接的 UI 组件都会重新渲染,即使它们只依赖状态中的部分数据。虽然可以使用 PureRenderMixin 或 reselect 来优化,但增加了额外的开发成本。
样板代码繁多:使用 Redux 需要编写大量的 action types、action creators、reducers,即使是一个简单的功能也需要完整的模板代码。
学习曲线陡峭:理解 Redux 的数据流需要掌握 action、reducer、middleware 等概念,对新手不太友好。
# 1.2 MobX的核心理念
MobX 采用了完全不同的设计理念:利用 JavaScript 的代理(Proxy)机制,自动追踪状态的变化并更新依赖它的组件。这种方式被称为响应式编程。
MobX 的核心理念可以概括为三点:
-
可观察的状态(Observable State):将普通的
JavaScript对象转换为可观察的对象,任何对状态的修改都能被追踪。 -
自动推导(Computed Values):根据现有状态自动计算衍生值,类似 Excel 的公式功能。
-
响应式副作用(Reactions):当状态变化时自动执行副作用操作,如更新
UI、打印日志、发起网络请求等。
# 1.3 MobX与Redux对比
| 特性 | MobX | Redux |
|---|---|---|
| 数据结构 | 多 store,支持普通对象 |
单一 store,不可变数据 |
| 更新方式 | 直接修改,可选严格模式 | 只能通过 action 和 reducer |
| 依赖追踪 | 自动追踪,按需更新 | 手动订阅,可能过度渲染 |
| 代码量 | 简洁,样板代码少 | 较多模板代码 |
| 学习成本 | 较低 | 较高 |
| 调试工具 | 友好的时间旅行调试 | 强大的 DevTools |
从数据流角度看,Redux 管理的是 STORE -> VIEW -> ACTION 的完整闭环,而 MobX 更关注 STORE -> VIEW 的部分。action 在 MobX 中是可选的,你可以直接修改状态,但这也带来了一个潜在问题:状态的修改入口不统一。
# 1.4 MobX的优缺点分析
优点
第一点是基于运行时的数据订阅。MobX 的数据依赖始终保持最小,而且是基于运行时自动追踪的。相比之下,使用 Redux 时可能一不小心就多订阅或者少订阅了数据,导致性能问题。因此在使用 Redux 时,我们需要借助 PureRenderMixin 以及 reselect 对 selector 做缓存优化。
第二点是通过面向对象的方式组织领域模型。OOP 的方式在某些场景下会比较方便,尤其是容易抽取 domain model 的时候。由于 MobX 支持引用方式引用数据,可以非常容易形成模型图(model graph),这有助于更好地理解应用结构。
第三点是修改数据方便自然。MobX 基于原生的 JavaScript 对象、数组和 Class 实现,修改数据不需要额外语法成本,也不需要始终返回一个新的数据,而是直接操作数据。
缺点
第一点是缺少最佳实践和社区积累。MobX 相对较新,遇到的问题可能社区都没有遇到过。并且,MobX 并没有很好的扩展和插件机制。
第二点是随意修改 store 的风险。我们都知道 Redux 里唯一可以改数据的地方是 reducer,这样可以保证应用的安全稳定;而 MobX 可以随意修改数据,触发更新,给人一种不安全的感觉。不过最新的 MobX 2.2 版本加入了 action 支持,并且开启 strict mode 之后,就只有 action 可以对数据进行修改,限制数据的修改入口,可以解决这个问题。
第三点是逻辑层的限制。如果更新逻辑不能很好地封装在 domain class 里,用 Redux 会更合适。另外,MobX 缺少类似 redux-saga 的库,业务逻辑的整合不知道放哪合适。
# 二、核心API详解
# 2.1 observable - 定义可观察状态
@observable 装饰器用于将 JavaScript 对象属性转换为可观察的属性。被装饰的属性会暴露出来供观察者使用,当属性值发生变化时,所有依赖该属性的地方都会收到通知。
Observable 值可以是 JavaScript 基本数据类型、引用类型、普通对象、类实例、数组和映射。
import { makeAutoObservable } from 'mobx'
class Counter {
// 基本类型
count = 0
name = '计数器'
// 引用类型
user = {
name: '张三',
age: 25
}
// 数组
items = []
constructor() {
// makeAutoObservable 会自动推断类型并添加相应的装饰器
makeAutoObservable(this)
}
increment() {
this.count++
}
decrement() {
this.count--
}
setName(name) {
this.name = name
}
}
const counter = new Counter()
如果使用装饰器模式,需要配置 babel 支持:
// 使用装饰器写法
import { observable, computed, action } from 'mobx'
class Counter {
@observable count = 0
@observable title = 'this is about page'
@observable num = 0
// computed - 计算值
@computed get getUserInfo() {
return `我是computed经过计算的getter, current num: ${this.num}`
}
// action - 动作
@action.bound add() {
this.num++
}
@action.bound reduce() {
this.num--
}
}
# 2.2 computed - 计算属性
计算值(Computed Values)是可以根据现有的状态或其它计算值衍生出的值。用于获取由基础 state 衍生出来的值,如果基础值没有变,获取衍生值时就会走缓存,这样就不会引起虚拟 DOM 的重新渲染。
import { makeAutoObservable, computed } from 'mobx'
class OrderStore {
items = []
taxRate = 0.1
constructor() {
makeAutoObservable(this, {
// 显式声明 computed 属性
subtotal: computed,
tax: computed,
total: computed
})
}
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
get tax() {
return this.subtotal * this.taxRate
}
get total() {
return this.subtotal + this.tax
}
addItem(item) {
this.items.push(item)
}
}
计算属性还支持 setter,用于实现逆向推导:
class Foo {
@observable length = 2
@computed get squared() {
return this.length * this.length
}
set squared(value) {
// 这是一个自动的动作,不需要注解
// 设置 squared 时,自动推导出 length
this.length = Math.sqrt(value)
}
}
# 2.3 action - 修改状态
只有 在 actions 中,才可以修改 MobX 中 state 的值。通过引入 MobX 定义的严格模式,可以强制使用 action 来修改状态,保证状态修改的可追溯性。
import { configure, makeAutoObservable, runInAction } from 'mobx'
// 开启严格模式 - 只有 action 可以修改状态
configure({ enforceActions: 'always' })
class Counter {
count = 0
constructor() {
makeAutoObservable(this)
}
increment() {
this.count++
}
decrement() {
this.count--
}
async fetchData() {
// 在异步操作中使用 runInAction
const data = await fetch('/api/data').then(r => r.json())
runInAction(() => {
this.data = data
})
}
}
runInAction 是一个实用的工具函数,用于在异步操作中批量更新状态:
import { runInAction } from 'mobx'
async function fetchUser() {
const user = await api.getUser()
// 所有的状态更新都放在 runInAction 中
runInAction(() => {
this.user = user
this.loading = false
this.error = null
})
}
# 2.4 observer - 响应式组件
observer 是由 mobx-react 包提供的高阶组件,用于将 React 组件转换为响应式组件。在组件的 render 函数中使用的任何 observable 发生变化时,组件都会自动重新渲染。
observer 的原理是用 mobx.autorun 包装了组件的 render 函数,确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
# 2.5 autorun - 自动运行
当可观察对象中保存的值发生变化时,可以在 mobx.autorun 中被观察到。autorun 在 observable 的值初始化或改变时自动运行。
如果你想响应式地产生一个可以被其它 observer 使用的值,请使用 @computed;如果你不想产生新值,而想要达到一个效果(如打印日志、发起网络请求),请使用 autorun。
# 2.6 reaction - 响应式副作用
# 2.7 flow - 异步流程管理
# 三、计数器完整示例
下面是一个完整的计数器示例,展示了如何将 MobX 与 React 结合使用: