参考:Ten minute introduction to MobX and React
Mobx是一种简单,可扩展且经过实战考验的状态管理解决方案。 本教程将在十分钟内教你MobX的所有重要概念。 MobX是一个独立的库,但大多数人都将它与React一起使用,本教程重点介绍了这种组合。
核心理念
状态是每个应用程序的核心,许多状态管理解决方案试图限制可以修改状态的方式,例如通过使状态不可变。
MobX要让状态管理变得简单起来,它解决了根本问题:不允许产生前后不一致的状态。实现这一目标的策略很简单:能从应用的状态(state)中导出的任何派生值(derivation)都是自动导出的。
- 首先有一个应用的状态(state),可以是任何objects,arrays,promitives,references等等能够构建你的程序的东西。这些值是你的应用程序的元数据(data cells)
- 其次是派生值(derivations),可以是任何能自动从你的状态(state)中计算得到的值。
- 反应(reaction)和derivations很像,不一样的是,reaction是一个动作,它用于制动执行一些任务,通常是一些I/O相关的任务,它们能够确保在合理的时间时,DOM能够自动更新或者网络请求能够自动执行。
- 动作(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());
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());
console.log(todoStore.completedTodosCount);
|
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修饰计算过程,以标记这些计算过程是从状态派生而来。
到此为止,我们还没用过pendingRequests
和assignee
。在构造函数中,我们创建了一个小函数,它自动打印报告。 由于report依赖于已被修饰为observable的todos状态,因此它会及时打印报告:
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
这个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 });
|
一开始,初始界面是:
当运行完第二行时:
第三行:
第四行:
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";
|