MobX是一个简单、可扩展、久经沙场的状态管理解决方案。这篇文章主要是翻译自mobx官网上的十分钟mobx教程来介绍Mobx以及自己在学习Mobx中的一些浅显总结。官网教程
跟MobX有关的其实只有@observer装饰器。但保证每个组件各自在相关数据变化时重新渲染已是足够了。
setTimeout(function() {
observableTodoStore.addTodo('Random Todo ' + Math.random());
observableTodoStore.pendingRequests--;
}, 2000);
https://mobx.js.org/getting-started.html
为什么使用Mobx
在使用Reactjs过程中,经常会遇到一些state相关的问题和bug。主要有以下几个问题:setState异步导致state更新不及时引起的bug
如果修改一些state,然后马上去调用某个state,得到的是之前的值,并不是修改后的值。这是因为setState是异步的原因,这种bug是经常易犯的错误。比如下面的代码,显示当前选中元素的值,实际效果显示的是上一个选中元素的值。import React from 'react';import logo from './logo.svg';import './App.css';class App extends React.Component {constructor(props, context) {super(props, context)this.state = {
selection: props.values[0]
};
}
render() {return (
this.onKeyDown} tabIndex={0}>
{this.props.values.map(value => className={value === this.state.selection ? 'selected' : ''}
key={value}
onClick={() => this.onSelect(value)}
>
{value}
)}
)
}
onSelect(value) {this.setState({
selection: value
};//此处,setState后在fireOnSelect中使用state的值,因此不是用户想要的结果this.fireOnSelect();
}
onKeyDown = (e) => {
const {values} = this.props
const idx = values.indexOf(this.state.selection)if (e.keyCode === 38 && idx > 0) { /* up */this.setState({
selection: values[idx - 1]
})
} else if (e.keyCode === 40 && idx -1) { /* down */this.setState({
selection: values[idx + 1]
})
}this.fireOnSelect();
}
fireOnSelect() {if (typeof this.props.onSelect === "function")this.props.onSelect(this.state.selection) /* not what you expected..*/
}
}
export default App;
ReactDOM.render("State.", "Should.", "Be.", "Synchronous."]}
onSelect={value => console.log(value)}
/>,
document.getElementById('root')
);
针对上面的问题,解决方法比较简单,可以使用一个变量来存储该值,或者使用setState的callback来实现。下面是修改后的代码:
onSelect(value) {this.setState({
selection: value
},()=>{this.fireOnSelect()
})
}
setState会引起不必要的渲染(render)state值的变化会使页面重新渲染,但很多状态的变化引起渲染是不必要的。粗略地说,认为一次重新渲染是必要的有以下原因:
- state新设置的值和上一次的值完全一样。这种情况通常可以通过实现shouldComponentUpdate生命周期来解决,或者你已经在使用一些库(pure render)来解决这个问题。
- 有时state的修改与UI显示无关。
- 并不是所有组件状态都应该使用setState存储和更新。很多复杂组件通常需要使用生命周期函数来管理一些内容,例如:计时器、网络请求、事件等。使用setState管理这些复杂组件的状态不仅会触发重新渲染,还会导致一些相关的生命周期函数再次被触发,进而导致一些奇怪的状况。
MobX的核心概念
Mobx和Redux一样,采用单向数据流管理状态:通过action改变应用的state,state的改变触发相应UI的更新。 状态管理是每个应用的核心,不一致的状态数据或者与本地变量不同步的情况会导致一些奇怪的bug,比如上面的例子。因此许多状态管理方案都尝试限制修改状态数据的方式,比如让状态数据不可变。MobX从根本上让状态管理变得简单:它不会再次建出一个不一致状态。方法也很简单:确保每个源自于应用状态的内容能被自动感知到。 State:存在一个应用级别的状态。对象图表、数组、原始值、引用组成了应用的模型数据。这些值是应用的数据单元。是应用依赖的最小状态集,没有任何多余的状态。 Derivation:也是Computed value任何值都能从应用状态里自动计算获得。这些派生,或者说是计算属性,可以是简单值,比如未完成事项的数量,也可以是像表现未完成事项的HTML展示这种复杂的内容。在电子表格里,这些就是应用的公式和图表。 计算 值 ,是根据state推导计算出来的值 Reaction:响应(reaction)跟派生很像。主要区别在于这些方法不会生产出值。相反,它们会自动运行一些任务。通常这些都是I/O相关的。它们保证DOM能被更新或者网络请求在合适的时间发出来。 Action:动作用来改变状态。MobX保证改变应用状态的动作能被所有派生和响应自动处理,同步并且不会出问题。 建议 是唯一可以修改状态的方式这些翻译过来的文字读起来比较绕口,但结合下面的例子就能理解每个概念的意思.
一步步演示MobX的使用
简单的todo应用
下面是官网上的具体实例,而且官网上提供了run code,可以看到每个Demo的演示效果。class TodoStore {
todos = [];
get completedTodosCount() {return this.todos.filter(todo => todo.completed === true
).length;
}
report() {if (this.todos.length === 0)return "";const nextTodo = this.todos.find(todo => todo.completed === false);return `Next todo: "${nextTodo ? nextTodo.task : ""}". ` +`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {this.todos.push({task: task,completed: false,assignee: null
});
}
}const todoStore = new TodoStore();
上面的代码是一个基本的数据模型,里面包含数据存储,以及改变数据存储的方法。在没有使用MobX之前,往数据模型中增加项目时,需要手动的调用report来跟踪变化。如下代码:
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可以追踪变化。我们让一些属性通过@observable装饰器变得可观察的了,并能主动告知MobX这些变化。计算属性通过@computed装饰器来自动从状态数据中派生。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 "";
const nextTodo = this.todos.find(todo => todo.completed === false);return `Next todo: "${nextTodo ? nextTodo.task : ""}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
addTodo(task) {this.todos.push({
task: task,
completed: false,
assignee: null
});
}
}
const observableTodoStore = new ObservableTodoStore();
在构造方法里我们创建了一个小方法用来打印report并用autorun包裹它。Autorun创建一个只运行一次的响应(reaction),它能在每次可观察数据变化后自动运行。因为report方法使用到了可观察的todos属性,它将会在合适的时机打印数据。下面的代码显示了这个特性。mobx生成依赖两个装饰器,@computed和@observable配合使用。简单点说,observable让变量可跟踪,computed,当observable变量变化就会调用一下去计算是否需要更新ui。
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方法。但注意observableTodoStore.todos[0].task = "grok MobX tutorial";没有任何的输出,尽管状态数据todos发生了变化,但是report方法却没有被触发,因为todos[0].task变化没有导致report的变化。
Next todo: "read MobX tutorial". Progress: 0/1Next todo: "read MobX tutorial". Progress: 0/2Next todo: "try MobX". Progress: 1/2Next todo: "try MobX in own project". Progress: 1/2
但如果改成下面的代码,report就会被调用。
这个例子很好体现了autorun不仅监测了todos数组对象,还检测了是否需要更新。
@computed get report() {if (this.todos.length === 0)return "";return `Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`;
}
使React也变成响应式
React组件没有做到开箱即用,mobx提供了一个装饰器,包裹了组件,让render方法自动调用,保证你的组件和状态的同步。使用了mobx之后,就不需要再调用setState了,组件会变得 “智能”。React的组件并不是开箱即为响应式的。mobx-react的@observer修饰器能让React组件的render方法放入autorun中,自动在组件和状态数据之间同步,同上面的report是异曲同工的效果。跟MobX有关的其实只有@observer装饰器。但保证每个组件各自在相关数据变化时重新渲染已是足够了。
@observerclass TodoList extends React.Component {
render() {
const store = this.props.store;return (
{ store.report }
{ store.todos.map((todo, idx) =>
) }
{ store.pendingRequests > 0 ? Loading... : null }this.onNewTodo }>New Todo (double-click a todo to edit)
);
} onNewTodo = () => {this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
}
@observerclass TodoView extends React.Component {
render() {
const todo = this.props.todo;return (this.onRename }> type='checkbox'
checked={ todo.completed }
onChange={ this.onToggleCompleted }
/>
{ todo.task }
{ todo.assignee
? { todo.assignee.name }
: null
}
);
} 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(,document.getElementById('reactjs-app')
);
下面的代码很好的显示了仅仅只要改变数据而不用做任何别的事情。MobX会自动计算衍生数据,用状态数据更新相关的用户界面。
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 });
引用
目前我们创建出了可观察的对象,数组和基础类型。你可能奇怪,MobX里引用是什么样子的?我的状态数据结构能是一种图的形式吗?之前你可能意识到todos数组上有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了。一个是people一个是todos。给assignee赋值的时候我们仅仅是用引用。但变化也能被自动更新到TodoView上。有了Mobx,就不需要把数据先范式化,再写selector来保证视图得到更新。实际上数据在哪里存储也根本不重要。只要对象是可观察的,MobX将会跟踪它们。JavaScript的引用也能被跟踪。MobX也将会自动跟踪衍生计算出来的数值。
异步操作
由于Todo应用里所有都是从状态数据里衍生出来的,所以状态什么时候更改并不重要。下面的代码很直观,我们用pendingRequests来让界面显示当前的加载状态。一旦加载完成,更新todos并且减去pendingRequests。和之前的TodoList代码比较一下,看看pendingRequests是如何使用的。observableTodoStore.pendingRequests++;setTimeout(function() {
observableTodoStore.addTodo('Random Todo ' + Math.random());
observableTodoStore.pendingRequests--;
}, 2000);
写在最后
这是一篇Mobx的入门文章,Mobx还有很多的高级功能没有涉及,比如跨多组件应用,跨多层级组件如何应用等,一起来慢慢学习。