参考: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。
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
| 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 的依赖性跟踪是动态的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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 可以跟踪正在进行的所有更改。
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
| 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 状态,因此它会及时打印报告:
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"
|
我们可以看到,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
,也不需要进行状态提升之类。
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
| @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
的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| observableTodoStore.todos = [ { task: "grok MobX tutorial", completed: true, assignee: null, }, { task: "try MobX in own project", complted: false, assignee: null, }, ]
observableTodooStore.pendingRequests = 0
|
我们来运行一下下面的代码:
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 })
|
一开始,初始界面是:
当运行完第二行时:
第三行:
第四行:
Working with references
在之前,所有可观察的对象,都是原始类型的值:字符串,boolean 值,数值等。但当我们依赖的状态只是一个对其他对象的引用的话,该如何?下面的代码展示了该如何监听一个对象。
1 2 3 4
| 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"
|