MobX
是一种简单的、可扩展的、久经考验的状态管理解决方案。
这个教程将在十分钟内向你详解 MobX 的所有重要概念。MobX 是一个独立的库,但是大部分人将它和 React 共同使用,所以本教程将重点讲解他们的结合使用。
State 是所有应用的核心,没有任何途径比“创建不稳定的 State 或者创建与周围本地变量不同步的State”更容易产生 bug 丛生、不可管理的应用了。
因此,许多 State 管理解决方案试图限制可以变更状态的方法,例如使其不可变(immutable)。
但这带来了新的问题:数据需要规范化,无法保证引用的完整性,使用原型之类的强大概念几乎是不可能的。
MobX 通过解决根本问题重新简化了 State 管理工作:我们根本无法创建不稳定的 State。
达到这一目标的策略很简单:保证从应用程序状态派生出的所有内容都可以被自动地推导出来。
原理上,MobX 将你的应用看做是一个电子表格:
理论讲完了,实际操作试试可能比仔细阅读上面的东西更能说明问题。出于创意,让我们从一个非常简单的 todo store 做起。注意:下面所有的代码块是可编辑的,可以点击 run code 按钮执行它们(译者注:臣妾做不到……详细执行结果请参考原文)。下面是一个很简单的 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();
我们刚刚创建了一个包含 待办事项
列表的一个 todoStore
实例。是时候给它填充一些对象了。为了保证我们可以看到我们改变的影响,我们在每个变更之后调用 todoStore.report
并打印它。注意这个报告故意只打印第一个任务。这使得这个例子看起来有点别扭,但是你将看到它可以很好地说明 MobX 的依赖跟踪是动态的。
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
,而是生命我们希望它在每次状态的改变时被调用呢?这将使我们不再需要纠结在所有可能影响报告的地方调用 report。我们想要保证最新的报告被打印。但是我们不想纠结于怎么去组织它。
值得庆幸的是,这正是 MobX 可以为你做到的。自动执行完全依赖 state 的代码。因此我们的 report
函数像电子表格中的图表一样自动更新。为了实现这一目标, TodoStore
需要变成可监视的(observable)以保证 MobX 可以追踪到所有改变。让我们一起改改代码来实现它。
进一步, completedTodosCount
属性可以由 todo list 自动推导而来。我们可以使用 @observable
和 @computed
装饰器为一个对象增加 observable 属性:
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();
搞定啦!我们为 MobX 标记了一些 @observable
属性,这些属性的值可以随时改变。计算值是用 @computed
标记以表示他们可以由 state 推导出来。
pendingRequests
和 assignee
属性现在还没被使用,但是将会在本教程的后面被使用。为了简洁,本页中的例子都使用了 ES6、JSX 和装饰器(decorators)。但是不要担心,MobX 中所有的装饰器对应有 ES5 的形式。
在构造函数中,我们创建了一个小函数来打印 report
并用 autorun
包裹它。autorun 创建了一个 响应(Reaction) 并执行一次,之后这个函数中任何 observable 数据变更时,响应都会被自动执行。由于 report
使用了 observable todos
属性,所以它将会在所有合适的时刻打印 report。下面的例子可以说明这一点,只需要点击一下 run 按钮(译者:……):
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";
很好玩对不对? report
自动地打印了,这个过程是自动的且没有中间变量泄露。如果你仔细研究日志,你会发现第四行没有生成新的日志行。因为 report 并没有 真正地 因为重命名而改变,尽管底层数据确实变了。而变更第一个 todo 的名字改变了 report,因为它的 name 被 report 使用了。这充分地说明了 autorun
不只监听了 todo
数组,而且还监听了 todo 元素中的个别属性。
好了,目前为止我们创建了一个简单的响应式 report。是时候在这个 store 周围构造一个响应式的用户接口了。React 组件无法对外界作出反应(除了自己的名字)。 mobx-react
包的 @observer
装饰器通过将 React 组件的 render
方法包裹在 autorun
中解决了这一问题,它自动地保持你的组件和 state 同步。理论上这和我们之前对 report
的做法没什么区别。
下面的例子定义了一些 React 组件。这些组件中只有 @observer
是属于的 MobX 的。但它足以保证所有的组件都可以在相关数据变更时独立地重新渲染。你不再需要调用 setState
,也不必考虑如何通过配置选择器或高阶组件来订阅应用程序 state 的适当部分。可以说,所有的组件都变得智能化。不过他们是以愚蠢的声明的方式定义的。
点击 Run code 按钮查看下面代码的结果。这个例子是可编辑的,所以你可以随便在里面玩耍。试着删掉所有的 @oberver
或者只删掉装饰 TodoView
的那一个。右边预览中的数字会在每次组件重新渲染的时候高亮。
@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 将会从 store 的 state 中自动地派生并更新用户界面相关的部分。
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...
到目前为止,我们已经创建了 observable 对象(包括原型和普通对象),数组和原语。你可能会惊讶,MobX 是如何操作这些引用的?是我们的 state 可以被用于创建一个图表吗?在上面的例子中,你可能发现 todo 上有一个 assignee
属性。让我们通过引入另一个包含人员信息的“store”(其实,它只是一个美化的数组)来给他们一些值,并将任务分配给他们。
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。一个包含人员信息,另一个包含 todo 信息。为人员 store 中的一个人赋予一个 assignee
,我们只需要添加一个引用。这些改变会被 TodoView
自动获取。在 MobX 的帮助下,我们不需要先格式化数据并写相应的选择器以保证我们的组件可以被更新。实际上,甚至是数据的存储位置也并不重要。只要对象被设置为 obervable,MobX 将可以追踪他们。真实的 JavaScript 引用将会起作用。如果它们与一个派生有关,那么 MobX 将自动地追踪它们。为了测试这一点,只需要尝试改变下面的 input 框中的名字(测试前先确保你点击了 Run Code 按钮!)。
由于我们的 Todo 小应用中的所有数据都是派生自 state,因此 state 何时改变并不重要。这使得创建异步操作变得异常简单。点击下面的按钮(多次)以模拟异步地创建新的待办项。
Load todo
后面的代码给常简单。我们首先通过更新 pendingRequests
这一 store 属性使 UI 显示当前的加载状态。当加载结束之后,沃恩跟新 store 中的待办项并再次减少 pendingRequests
计数。将这段代码与上面的 TodoList
定义相比较以学习如何使用 pendingRequests 属性。
observableTodoStore.pendingRequests++;
setTimeout(function() {
observableTodoStore.addTodo('Random Todo ' + Math.random());
observableTodoStore.pendingRequests--;
}, 2000);
mobx-react-devtools
包提供了一个被用于 MobX + ReactJS 应用的显示在屏幕右上角的开发工具。点击第一个按钮将会高亮每一个被重新渲染的 @observer
组件。如果你点击第二个按钮,预览中的组件依赖树将会显示出来,你可以在任何时候准确地检测出它正在观察的是哪一段数据。
就这么多!没有样板。只有一些简单的声明式组件用来形成我们整体的 UI。这份 UI 完全响应式地派生自我们的 state。你现在可以开始在你的应用中使用 mobx
和 mobx-react
包啦。下面对你目前学到的东西做一个简要总结:
@observable
装饰器或 observable(objectorarray)
函数使 MobX 可以追踪对象。@computed
装饰器可被用于创建基于 state 自动计算值的函数。autorun
来自动地运行依赖于 observable state 的函数。这对于打日志、发起网络请求等来说很有用。mobx-react
包中的 @observer
装饰器将你的 React 组件变得真正的可响应。他们将会自动并有效地更新。即使是在用够大量数据的大型复杂项目中。多花点时间玩玩上面的可编辑代码块,以对 MobX 如何对你的操作作出响应有一个基本的概念。例如,你可以为 report 函数增加一个 log 语句来看它什么时候被调用;或者完全不要显示 report
来看看会对 TodoList
的渲染造成什么影响;或者在某些情况下不要显示它……
人们通常将 MobX 当做是 Redux 的替代。但是请注意,MobX 只是一个解决技术问题的库,其本身并没有 state 容器。从这个意义上说,上面的例子是人为设计的,所以我们建议您使用适当的工程实践,如在方法中封装逻辑、在 store 或控制器中组织它们等等。或者,像 HackerNews 中某位用户说的:
“MobX,它总是被提起,但我忍不住要赞美它。使用 MobX 写东西意味着它可以完成所有控制器(controllers) / 调度员(dispatchers) / 操作(actions) / 管理程序(supervisors) 以及其他管理数据流需要考虑的工作,而不只是完成一个 Todo 应用默认要求完成的那些工作。”
往期精选文章 |
---|
使用虚拟dom和JavaScript构建完全响应式的UI框架 |
扩展 Vue 组件 |
使用Three.js制作酷炫无比的无穷隧道特效 |
一个治愈JavaScript疲劳的学习计划 |
全栈工程师技能大全 |
WEB前端性能优化常见方法 |
一小时内搭建一个全栈Web应用框架 |
干货:CSS 专业技巧 |
四步实现React页面过渡动画效果 |
让你分分钟理解 JavaScript 闭包 |
小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有