十分钟了解MobX

参考:Ten minute introduction to MobX and React

Mobx是一种简单,可扩展且经过实战考验的状态管理解决方案。 本教程将在十分钟内教你MobX的所有重要概念。 MobX是一个独立的库,但大多数人都将它与React一起使用,本教程重点介绍了这种组合。

核心理念

状态是每个应用程序的核心,许多状态管理解决方案试图限制可以修改状态的方式,例如通过使状态不可变。

MobX要让状态管理变得简单起来,它解决了根本问题:不允许产生前后不一致的状态。实现这一目标的策略很简单:能从应用的状态(state)中导出的任何派生值(derivation)都是自动导出的。

  1. 首先有一个应用的状态(state),可以是任何objects,arrays,promitives,references等等能够构建你的程序的东西。这些值是你的应用程序的元数据(data cells)
  2. 其次是派生值(derivations),可以是任何能自动从你的状态(state)中计算得到的值。
  3. 反应(reaction)derivations很像,不一样的是,reaction是一个动作,它用于制动执行一些任务,通常是一些I/O相关的任务,它们能够确保在合理的时间时,DOM能够自动更新或者网络请求能够自动执行。
  4. 动作(actions),只有actions能够去改变state,MobX确保所有state的变化都是通过action进行,并且会自动地、同步地被传递给derivation和reaction。

一个简单的例子:todo store

下面是一个简单的TodoStore,维护了一系列待办事项,还没引入MobX。

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并记录它。 请注意,report有意始终仅打印第一个任务。 它使这个例子有点人为,但正如你将在下面看到的,它很好地证明了MobX的依赖性跟踪是动态的。

todoStore.addTodo("read MobX tutorial");
console.log(todoStore.report()); // Next todo: "read MobX tutorial". Progress: 0/1

todoStore.addTodo("try MobX");
console.log(todoStore.report()); // Next todo: "read MobX tutorial". Progress: 0/2

todoStore.todos[0].completed = true;
console.log(todoStore.report()); // Next todo: "read MobX tutorial". Progress: 1/2

todoStore.todos[1].task = "try MobX in own project";
console.log(todoStore.report()); // Next todo: "read MobX tutorial". Progress: 1/2

todoStore.todos[0].task = "grok MobX tutorial";
console.log(todoStore.report()); // Next todo: "grok MobX tutorial". Progress: 1/2

console.log(todoStore.completedTodosCount); // 1

Becoming reactive

到目前为止,这段代码并没有什么特别之处。 但是,如果我们不显式地调用report,我们可以在每次状态更改时自动调用它吗?我们希望确保打印最新的报告,但却不想自己来组织这件事情。

幸运的是,这正是MobX可以做的,它可以自动执行依赖于state的代码。这样我们的report就会自动更新,就像电子表格中的图表一样。为了实现这一点,TodoStore必须变得可观察,以便MobX可以跟踪正在进行的所有更改。

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修饰计算过程,以标记这些计算过程是从状态派生而来。

到此为止,我们还没用过pendingRequestsassignee。在构造函数中,我们创建了一个小函数,它自动打印报告。 由于report依赖于已被修饰为observable的todos状态,因此它会及时打印报告:

observableTodoStore.addTodo("read MobX tutorial"); // Next todo: "read MobX tutorial". Progress: 0/1
observableTodoStore.addTodo("try MobX"); // Next todo: "read MobX tutorial". Progress: 0/2
observableTodoStore.todos[0].completed = true; // Next todo: "read MobX tutorial". Progress: 1/2
observableTodoStore.todos[1].task = "try MobX in own project";
observableTodoStore.todos[0].task = "grok MobX tutorial"; // Next todo: "grok MobX tutorial". Progress: 1/2

我们可以看到,report这个reaction,依赖于this.todos.length, this.todos[0].task, this.completedTodosCount,只有当这三个值发生改变时,才会自动调用report。而我们看到上面的第四行代码,其只改变了todos[1].task,并没有改变report所依赖的任何state或reaction,故而没有调用report

Making React reactive

mobx-react能够使得React的组件自动进行渲染,从而使得组件与其依赖的状态能够同步。

下面的代码定义了一些React组件,唯一不同的地方就是用了MobX的@observer装饰器,它使得每个组件能够在其相关的数据改变的时候重新渲染。这时我们不再需要setState,也不需要进行状态提升之类。

@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')
);

我们看到,TodoList组件依赖于前面的observableTodoStore.todos, observableTodoStore.pendingRequests,故而当observableTodoStore中的todos, pendingRequests发生任何改变时,都会引起TodoList的重新渲染。

在运行下面的代码时,回顾一下上面observableTodoStore的结果:

observableTodoStore.todos = [{
task: 'grok MobX tutorial',
completed: true,
assignee: null
}, {
task: 'try MobX in own project',
complted: false,
assignee: null
}];

observableTodooStore.pendingRequests = 0;

我们来运行一下下面的代码:

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...

一开始,初始界面是:

当运行完第二行时:

第三行:

第四行:

Working with references

在之前,所有可观察的对象,都是原始类型的值:字符串,boolean值,数值等。但当我们依赖的状态只是一个对其他对象的引用的话,该如何?下面的代码展示了该如何监听一个对象。

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";