简介

MobX 通过函数响应式编程的思想使状态管理变得简单和可扩展。MobX 背后的哲学是:任何可以从应用程序的状态中获取/衍生的数据都应该可以自动被获取/衍生。和 Redux 一样,MobX 也是采用单向数据流管理状态:通过 action 改变应用的state,state 的改变进而会导致受其影响的 views 更新,如图10-1所示。

image 2024 04 24 22 29 14 555
Figure 1. 图10-1

MobX 包含的主要概念有 4 个:state(状态)、computed value(计算值)、reaction(响应)和 action(动作)。computed value 和 reaction 会自动根据 state 的改变做最小化的更新,并且这个更新过程是同步执行的,也就是说,action 更改 state 后,新的 state 是可以被立即获取的。注意,computed value 采用的是延迟更新,只有当 computed value 被使用时它的值才会被重新计算,当 computed value 不再被使用时(例如使用它的组件已经被卸载),它将会被自动回收。computed value 必须是纯函数,不能使用它修改 state。

一般来说,驱动应用的任何数据都可以称为 state(状态)。但 MobX 中提到的 state 实际上都是指可观测的 state,因为对于不可观测的 state,它们的修改并不会自动产生影响,对 MobX 的数据流来说是没有意义的。

MobX 中大量使用了 ES.Next 的装饰器语法,但装饰器语法目前还处于试验阶段,create-react-app 创建的项目默认是不支持的。我们先来解决这个问题再继续介绍 MobX。要支持装饰器语法,可以使用 npm run eject 命令将项目配置 “弹出”,然后添加 babel-plugin-transform-decorators-legacy 这个 Babel 插件,也可以使用 custom-react-scripts(https://www.npmjs.com/package/custom-react-scripts)来创建项目。本书使用 custom-react-scripts 这种方式。具体方式为,在使用 create-react-app 创建项目时,指定 --scripts-version 参数的值为 custom-react-scripts:

create-react-app my-app --scripts-version custom-react-scripts

创建的项目根路径下有一个名为 .env 的文件,这个文件中定义了 custom-react-scripts 为项目新增的特性,例如支持装饰器语法、支持 Less、支持 Sass 等。打开这个文件,可以发现有一项配置是 REACT_APP_DECORATORS = true,这项配置就是用来启用装饰器语法的。

另外,很多编辑器遇到装饰器语法会提示错误,需要进行额外设置以支持装饰器语法。例如,本书使用的 VS Code 需要在项目的根路径下创建一个文件 jsconfig.json,文件的内容为:

{
    "compilerOptions": {
        "experimentalDecorators": true
    }
}

现在,我们就可以在项目中随意使用装饰器语法了。

我们通过 todos 应用来简单介绍这 4 个概念。本节项目的源码目录为 /chapter-10/todos-mobx。

MobX 可以使用 object、array、class 等任意数据结构定义可观测的 state。例如,使用 class 定义一个可观测的 state Todo 代表一项任务:

import { observable } from "mobx";

class Todo {
    id = Math.random();
    @observable title = "";
    @observable finished = false;
}

这里使用的 @observable 就属于装饰器语法,你也可以不使用它,直接使用 MobX 提供的函数定义一个可观测的状态。例如,下面是用 ES 5 语法实现的等价代码:

import { extendObservable } from "mobx";

function Todo() {
    this.id = Math.random();
    extendObservable(this, {
        title: "",
        finished: false
    });
}

显然,使用装饰器的代码更为清晰简洁,MobX 使用了大量装饰器语法,这也是官方推荐的方式,本书也是使用装饰器语法完成 MobX 的项目代码。经过 @observable 的修饰之后,Todo 的 title 和 finished 两个属性变成可观测状态(注意属性和状态的概念,状态对象的属性也是状态),它们的改变会自动被观察者获知。id 没有被 @observable 修饰,所以只是一个普通属性。

基于可观测的 state 可以创建 computed value。例如,todos 中需要获取未完成的任务总数,使用 @computed 定义一个 unfinishedTodoCount 的 computed value,计算未完成的任务总数:

import { observable, computed } from "mobx";

class TodoList {
    @observable todos = [];

    // 根据 todos 和 todo.finished 两个 state,创建 computed value
    @computed get unfinishedTodoCount() {
        return this.todos.filter(todo => !todo.finished).length;
    }
}

这里又定义了一个新的 state:TodoList。TodoList 的属性 todos 是一个可观测的数组,它的元素是前面定义的 Todo 的实例对象。当 todos 中的元素数量发生变化或某一个 todo 元素的 finished 属性变化时,unfinishedTodoCount 都会自动更新(更严谨的说法是,在需要时才自动更新,后面还会介绍)。

除了 computed value 会响应 state 的变化外,reaction 也会响应 state 的变化,不同的是,reaction 并不创建一个新值,而是用来执行有副作用的逻辑,例如输出日志到控制台、发送网络请求、根据 React 组件树更新 DOM 等。mobx-react 包提供了 @observer 装饰器和 observer 函数,可以将 React 组件封装成 reaction,自动根据 state 的变化更新组件 UI。例如,创建 TodoListView 和 TodoView 两个组件(也是两个 reaction)代表应用的 UI:

import React, { Component } from "react";
import ReactDOM from "react-dom";
import { observer } from "mobx-react";
import { observable, computed, action } from "mobx";

// 使用 @observer 装饰器创建 reaction
@observer
class TodoListView extends Component {
  render() {
    return (
      <div>
        <ul>
          {this.props.todoList.todos.map(todo => (
            <TodoView todo={todo} key={todo.id} />
          ))}
        </ul>
        剩余任务: {this.props.todoList.unfinishedTodoCount}
      </div>
    );
  }
}

// 使用 observer 函数创建 reaction
const TodoView = observer(({ todo }) => {
  const handleClick = action(() => (todo.finished = !todo.finished));
  return (
    <li>
      <input type="checkbox" checked={todo.finished} onClick={handleClick} />
      {todo.title}
    </li>
  );
});

const store = new TodoList();
store.todos.push(new Todo("Task1"));
store.todos.push(new Todo("Task2"));

ReactDOM.render(
  <TodoListView todoList={store} />,
  document.getElementById("root")
);

TodoListView 使用到的可观测 state 是 todos 和 todo.finished(通过 unfinishedTodoCount 间接使用),因此它们的改变将会更新 TodoListView 代表的 DOM,同样地,todo.finished 和 todo.title 的改变会更新使用这个 todo 对象的 TodoView 代表的 DOM。

MobX 通过 action 改变 state。我们在 TodoView 中定义一个 action,用来改变 todo.finish:

// 使用 observer 函数创建 reaction
const TodoView = observer(({ todo }) => {
  const handleClick = action(() => (todo.finished = !todo.finished));
  return (
    <li>
      <input type="checkbox" checked={todo.finished} onClick={handleClick} />
      {todo.title}
    </li>
  );
});

handleClick 就是用来改变状态 todo.finish 的 action,一般习惯使用 MobX 提供的 action 函数包裹应用中定义的 action。至此,这个精简版的 todos 应用已经包含了 MobX 涉及的主要概念。