接触了Vue之后,才了解了Mobx的意义,所以重新翻译此文,愿一同上路

原文地址,略有删节。

MobX是一个简单、可扩展、久经沙场的状态管理解决方案。这篇教程将会花十分钟指导你所有关于MobX的重要概念。MobX是一个独立的库,但大多数人还是将它和React配合使用,这篇教程也是会关注在两者的结合上。

核心概念

状态管理是每个应用的核心,不一致的状态数据或者与本地变量不同步的情况会很快导致充满bug且无法管理的应用。因此许多状态管理方案都尝试限制修改状态数据的方式,比如让状态数据不可变。但这样引入一些新问题,数据需要规范化,引用完整性也得不到保证,使用prototype这些强大功能也几乎变得不可能。

MobX从根本上让状态管理变得简单:它不会再次建出一个不一致状态。方法也很简单:确保每个源自于应用状态的内容能被自动感知到。

从概念上来说,MobX对待你的应用像一个电子表格。

  1. 首先,存在一个应用级别的状态。对象图表、数组、原始值、引用组成了应用的模型数据。这些值是应用的数据单元

  2. 然后就有了派生(derivation)。基本上,任何值都能从应用状态里自动计算获得。这些派生,或者说是计算属性,可以是简单值,比如未完成事项的数量,也可以是像表现未完成事项的HTML展示这种复杂的内容。在电子表格里,这些就是应用的公式和图表。

  3. 响应(reaction)跟派生很像。主要区别在于这些方法不会生产出值。相反,它们会自动运行一些任务。通常这些都是I/O相关的。它们保证DOM能被更新或者网络请求在合适的时间发出来。

  4. 最后是动作(action)。动作用来改变状态。MobX保证改变应用状态的动作能被所有派生和响应自动处理,同步并且不会出问题。

简单的todo应用

理论到此为止,实干兴邦。我们创建一个简单的ToDo应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class TodoStore {
todos = [];

get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}

report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}

addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}

const todoStore = new TodoStore();

我们用todo集合创建了一个todoStore实例。现在是往里面填充一些内容的时刻了。为保证看到变化,我们在每次变化后用todoStore.report打印日志。注意到每次只会打印出第一条。这个例子有点特意人为化,但是很好展示了MobX的依赖追踪是动态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report());

todoStore.addTodo("try MobX");
console.log(todoStore.report());

todoStore.todos[0].completed = true;
console.log(todoStore.report());

todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report());

todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report());

变成响应式

目前这些代码还没有什么特别的。但如果我们不是非得是以显式的方式,而是可以在每次状态数据变化后自动调用report方法岂不是更妙?这样的话将会把我们解放出来。我们希望保证最新的数据被日志打印,但又怕麻烦去组织这行为。

幸好有MobX来帮我们。根据状态数据可以自动地执行代码。然后我们的report方法里的打印自动更新就像电子表格一样。为实现这个目标,TodoStore需要变成可观察的,使得MobX可以追踪变化。让我们修改一下代码来实现它。

另外,completedTodosCount属性能自动的从todo列表里衍生计算出来。通过使用@observable和@computed修饰器我们可以引入可观察属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ObservableTodoStore {
@observable todos = [];
@observable pendingRequests = 0;

constructor() {
mobx.autorun(() => console.log(this.report));
}

@computed get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}

@computed get report() {
if (this.todos.length === 0)
return "<none>";
return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}

addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}


const observableTodoStore = new ObservableTodoStore();

就是这样!我们让一些属性通过@observable装饰器变得可观察的了,并能主动告知MobX这些变化。计算属性通过@computed装饰器来自动从状态数据中派生。

目前还没有使用pendingRequests和assignee属性。为简化例子,我们使用了ES6,JSX和装饰器。不用担心,所有的装饰器在MobX里也有对应的ES5的实现。

在构造方法里我们创建了一个小方法用来打印report并用autorun包裹它。Autorun创建一个只运行一次的响应(reaction),但是能在每次可观察数据变化后自动运行。因为report方法使用到了可观察的todos属性,它将会在合适的时机打印数据。下面的代码显示了这个特性。

1
2
3
4
5
observableTodoStore.addTodo("read MobX tutorial");
observableTodoStore.addTodo("try MobX");
observableTodoStore.todos[0].completed = true;
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial";

很有趣,对吧?日志被自动打印,同时也没有遗漏中间过程的数据。如果你仔细研究日志,你会发现第四行代码(译者注: observableTodoStore.todos[0].task = “grok MobX tutorial”;,从0计数的)并没有触发一条新的日志,因为report实际上没有被触发调用,尽管背后的数据的确是被替换了。另一个方面,修改第一条todo的name属性会触发report,因为name属性在report里被使用到了。这个例子很好体现了autorun不仅监测了todos数组对象,还监测了其中的特定的字段。

让React也变成响应式

现下我们仅是做了一个很傻很天真的响应式日志。是时候表演真正的技术了!来做一个界面展示出来。React的组件并不是开箱即为响应式的。mobx-react的@observer修饰器能让React组件的render方法放入autorun中,自动在组件和状态数据之间同步。在概念上和刚才的report没有什么区别。

下面的代码定义了一些React组件。跟MobX有关的其实只有@observer装饰器。但保证每个组件各自在相关数据变化时重新渲染已是足够了。不再需要调用setState,也不要搞明白如何用selector订阅或者注入高阶组件之类的东西。几乎所有的组件都变得聪明起来。尽管它们以一种傻傻的方式来被定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@observer
class TodoList extends React.Component {
render() {
const store = this.props.store;
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ idx } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
<button onClick={ this.onNewTodo }>New Todo</button>
<small> (double-click a todo to edit)</small>
<RenderCounter />
</div>
);
}

onNewTodo = () => {
this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
}

@observer
class TodoView extends React.Component {
render() {
const todo = this.props.todo;
return (
<li onDoubleClick={ this.onRename }>
<input
type='checkbox'
checked={ todo.completed }
onChange={ this.onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? <small>{ todo.assignee.name }</small>
: null
}
<RenderCounter />
</li>
);
}

onToggleCompleted = () => {
const todo = this.props.todo;
todo.completed = !todo.completed;
}

onRename = () => {
const todo = this.props.todo;
todo.task = prompt('Task name', todo.task) || todo.task;
}
}

ReactDOM.render(
<TodoList store={ observableTodoStore } />,
document.getElementById('reactjs-app')
);

下面的代码很好的显示了仅仅只要改变数据而不用做任何别的事情。MobX会自动计算衍生数据,用状态数据更新相关的用户界面。

1
2
3
4
5
 const store = observableTodoStore;
store.todos[0].completed = !store.todos[0].completed;
store.todos[1].task = "Random todo " + Math.random();
store.todos.push({ task: "Find a fine cheese", completed: true });
// etc etc.. add your own statements here...

引用

目前我们创建出了可观察的对象,数组和基础类型。你可能奇怪,MobX里引用是什么样子的?我的状态数据结构能是一种图的形式吗?之前你可能意识到todos数组上有assignee字段。我们引入另一个store包含被赋予任务的人。

1
2
3
4
5
6
7
var peopleStore = mobx.observable([
{ name: "Michel" },
{ name: "Me" }
]);
observableTodoStore.todos[0].assignee = peopleStore[0];
observableTodoStore.todos[1].assignee = peopleStore[1];
peopleStore[0].name = "Michel Weststrate";

现在有两个独立的store了。一个是people一个是todos。给assignee赋值的时候我们仅仅是用引用。但变化也能被自动更新到TodoView上。有了Mobx,就不需要把数据先范式化,再写selector来保证视图得到更新。实际上数据在哪里存储也根本不重要。只要对象是可观察的,MobX将会跟踪它们。JavaScript的引用也能被跟踪。MobX也将会自动跟踪衍生计算出来的数值。

异步操作

由于Todo应用里所有都是从状态数据里衍生出来的,所以状态什么时候更改并不重要。

略去按钮操作部分

下面的代码很直观,我们用pendingRequests来让界面显示当前的加载状态。一旦加载完成,更新todos并且减去pendingRequests。和之前的TodoList代码比较一下,看看pendingRequests是如何使用的。

1
2
3
4
5
observableTodoStore.pendingRequests++;
setTimeout(function() {
observableTodoStore.addTodo('Random Todo ' + Math.random());
observableTodoStore.pendingRequests--;
}, 2000);

开发工具

以下工具可以被用来分析你的mobx-react应用

  • 组件的重复渲染可视化是React devtools的一部分了

  • React devtools也显示组件依赖树关系

  • 事件仍然可以通过mobx-logger或者手工使用mobx的spy或trace方法用浏览器插件查看

mobx-react-devtools不再被支持了。

总结

这就是全部!没有标杆代码。只有一些简单的组件组成了我们全部的界面,所有的一切都从状态数据里衍生出来。你现在可以在应用里使用Mobx和mobx-react了。简单的总结如下

  • 使用@observable装饰器或者observable(object / array)方法来使得Mobx对象可追踪

  • @computed装饰器可以被用来修饰从状态数据衍生出来的创建方法

  • 使用autorun来根据可观察状态自动运行方法。对于日志,请求网络等行为很有用

  • 使用mobx-react的@observer装饰器来让React组件也变响应式。它们能自动有效的更新,即便是在大型复杂的应用中使用大量的数据

MobX不是一个状态容器

人们经常把MobX当做是Redux的替代。但要注意到MobX仅是一个解决技术难题的库,而并不是一种状态容器的架构。从这个意义上来说,上面的例子太做作了,推荐使用合适的练习方式,比如在方法里封装逻辑,在store或controller里组织数据。