简介
MobX 通过函数响应式编程的思想使状态管理变得简单和可扩展。MobX 背后的哲学是:任何可以从应用程序的状态中获取/衍生的数据都应该可以自动被获取/衍生。和 Redux 一样,MobX 也是采用单向数据流管理状态:通过 action 改变应用的state,state 的改变进而会导致受其影响的 views 更新,如图10-1所示。

MobX 包含的主要概念有 4 个:state(状态)、computed value(计算值)、reaction(响应)和 action(动作)。computed value 和 reaction 会自动根据 state 的改变做最小化的更新,并且这个更新过程是同步执行的,也就是说,action 更改 state 后,新的 state 是可以被立即获取的。注意,computed value 采用的是延迟更新,只有当 computed value 被使用时它的值才会被重新计算,当 computed value 不再被使用时(例如使用它的组件已经被卸载),它将会被自动回收。computed value 必须是纯函数,不能使用它修改 state。
一般来说,驱动应用的任何数据都可以称为 state(状态)。但 MobX 中提到的 state 实际上都是指可观测的 state,因为对于不可观测的 state,它们的修改并不会自动产生影响,对 MobX 的数据流来说是没有意义的。 |
MobX 中大量使用了 ES.Next 的装饰器语法,但装饰器语法目前还处于试验阶段,create-react-app 创建的项目默认是不支持的。我们先来解决这个问题再继续介绍 MobX。要支持装饰器语法,可以使用 npm run eject 命令将项目配置 “弹出”,然后添加 babel-plugin-transform-decorators-legacy 这个 Babel 插件,也可以使用 custom-react-scripts(https://www.npmjs.com/package/custom-react-scripts)来创建项目。本书使用 custom-react-scripts 这种方式。具体方式为,在使用 create-react-app 创建项目时,指定 --scripts-version 参数的值为 custom-react-scripts:
create-react-app my-app --scripts-version custom-react-scripts
创建的项目根路径下有一个名为 .env 的文件,这个文件中定义了 custom-react-scripts 为项目新增的特性,例如支持装饰器语法、支持 Less、支持 Sass 等。打开这个文件,可以发现有一项配置是 REACT_APP_DECORATORS = true,这项配置就是用来启用装饰器语法的。
另外,很多编辑器遇到装饰器语法会提示错误,需要进行额外设置以支持装饰器语法。例如,本书使用的 VS Code 需要在项目的根路径下创建一个文件 jsconfig.json,文件的内容为:
{
"compilerOptions": {
"experimentalDecorators": true
}
}
现在,我们就可以在项目中随意使用装饰器语法了。
我们通过 todos 应用来简单介绍这 4 个概念。本节项目的源码目录为 /chapter-10/todos-mobx。
MobX 可以使用 object、array、class 等任意数据结构定义可观测的 state。例如,使用 class 定义一个可观测的 state Todo 代表一项任务:
import { observable } from "mobx";
class Todo {
id = Math.random();
@observable title = "";
@observable finished = false;
}
这里使用的 @observable 就属于装饰器语法,你也可以不使用它,直接使用 MobX 提供的函数定义一个可观测的状态。例如,下面是用 ES 5 语法实现的等价代码:
import { extendObservable } from "mobx";
function Todo() {
this.id = Math.random();
extendObservable(this, {
title: "",
finished: false
});
}
显然,使用装饰器的代码更为清晰简洁,MobX 使用了大量装饰器语法,这也是官方推荐的方式,本书也是使用装饰器语法完成 MobX 的项目代码。经过 @observable 的修饰之后,Todo 的 title 和 finished 两个属性变成可观测状态(注意属性和状态的概念,状态对象的属性也是状态),它们的改变会自动被观察者获知。id 没有被 @observable 修饰,所以只是一个普通属性。
基于可观测的 state 可以创建 computed value。例如,todos 中需要获取未完成的任务总数,使用 @computed 定义一个 unfinishedTodoCount 的 computed value,计算未完成的任务总数:
import { observable, computed } from "mobx";
class TodoList {
@observable todos = [];
// 根据 todos 和 todo.finished 两个 state,创建 computed value
@computed get unfinishedTodoCount() {
return this.todos.filter(todo => !todo.finished).length;
}
}
这里又定义了一个新的 state:TodoList。TodoList 的属性 todos 是一个可观测的数组,它的元素是前面定义的 Todo 的实例对象。当 todos 中的元素数量发生变化或某一个 todo 元素的 finished 属性变化时,unfinishedTodoCount 都会自动更新(更严谨的说法是,在需要时才自动更新,后面还会介绍)。
除了 computed value 会响应 state 的变化外,reaction 也会响应 state 的变化,不同的是,reaction 并不创建一个新值,而是用来执行有副作用的逻辑,例如输出日志到控制台、发送网络请求、根据 React 组件树更新 DOM 等。mobx-react 包提供了 @observer 装饰器和 observer 函数,可以将 React 组件封装成 reaction,自动根据 state 的变化更新组件 UI。例如,创建 TodoListView 和 TodoView 两个组件(也是两个 reaction)代表应用的 UI:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable, computed, action } from "mobx";
// 使用 @observer 装饰器创建 reaction
@observer
class TodoListView extends Component {
render() {
return (
<div>
<ul>
{this.props.todoList.todos.map(todo => (
<TodoView todo={todo} key={todo.id} />
))}
</ul>
剩余任务: {this.props.todoList.unfinishedTodoCount}
</div>
);
}
}
// 使用 observer 函数创建 reaction
const TodoView = observer(({ todo }) => {
const handleClick = action(() => (todo.finished = !todo.finished));
return (
<li>
<input type="checkbox" checked={todo.finished} onClick={handleClick} />
{todo.title}
</li>
);
});
const store = new TodoList();
store.todos.push(new Todo("Task1"));
store.todos.push(new Todo("Task2"));
ReactDOM.render(
<TodoListView todoList={store} />,
document.getElementById("root")
);
TodoListView 使用到的可观测 state 是 todos 和 todo.finished(通过 unfinishedTodoCount 间接使用),因此它们的改变将会更新 TodoListView 代表的 DOM,同样地,todo.finished 和 todo.title 的改变会更新使用这个 todo 对象的 TodoView 代表的 DOM。
MobX 通过 action 改变 state。我们在 TodoView 中定义一个 action,用来改变 todo.finish:
// 使用 observer 函数创建 reaction
const TodoView = observer(({ todo }) => {
const handleClick = action(() => (todo.finished = !todo.finished));
return (
<li>
<input type="checkbox" checked={todo.finished} onClick={handleClick} />
{todo.title}
</li>
);
});
handleClick 就是用来改变状态 todo.finish 的 action,一般习惯使用 MobX 提供的 action 函数包裹应用中定义的 action。至此,这个精简版的 todos 应用已经包含了 MobX 涉及的主要概念。