Yves yao

MobX:MobX 和 React 十分钟快速入门

原文链接: mobx.js.org

MobX 是一种简单的、可扩展的、久经考验的状态管理解决方案。

这个教程将在十分钟内向你详解 MobX 的所有重要概念。MobX 是一个独立的库,但是大部分人将它和 React 共同使用,所以本教程将重点讲解他们的结合使用。

核心理念

State 是所有应用的核心,没有任何途径比“创建不稳定的 State 或者创建与周围本地变量不同步的State”更容易产生 bug 丛生、不可管理的应用了。

因此,许多 State 管理解决方案试图限制可以变更状态的方法,例如使其不可变(immutable)。

但这带来了新的问题:数据需要规范化,无法保证引用的完整性,使用原型之类的强大概念几乎是不可能的。

MobX 通过解决根本问题重新简化了 State 管理工作:我们根本无法创建不稳定的 State。

达到这一目标的策略很简单: 保证从应用程序状态派生出的所有内容都可以被自动地推导出来。

原理上,MobX 将你的应用看做是一个电子表格:

  1. 首先,我们看应用状态(application state)。对象,数组,原型,引用组成了你的应用程序的 model。
  2. 其次,看看推导(derivations)。讲道理,所有可以通过应用程序 state 自动计算出来的值都算推导。这些推导或计算的值,范围包括从简单的值(如未完成的 todo 数量),到复杂的值(如一个表示 todo 的可视化 HTML)。从电子表格的角度看:这些是应用程序的公式和图表。
  3. 响应(Reactions) 与推导很类似。主要的区别是这些函数不产生值,而是自动地执行一些任务,这些任务通常与 I/O 相关。他们保证了在正确的时间自动地更新 DOM 或者发起网络请求。
  4. 最后我们看看 行动(actions)。行动是所有改变 state 的事情。MobX 将保证所有由你的操作触发的 state 变更都可以被所有的派生和响应处理。这个过程是同步且无故障的。

一个简单的 todo store

理论讲完了,实际操作试试可能比仔细阅读上面的东西更能说明问题。出于创意,让我们从一个非常简单的 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());

Becoming reactive

到目前为止,这段代码没什么特殊的。但是如果我们不需要明确地调用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 推导出来。

pendingRequestsassignee 属性现在还没被使用,但是将会在本教程的后面被使用。为了简洁,本页中的例子都使用了 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 元素中的个别属性。

让 React 变得有响应(reactive)

好了,目前为止我们创建了一个简单的响应式 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 何时改变并不重要。这使得创建异步操作变得异常简单。点击下面的按钮(多次)以模拟异步地创建新的待办项。


<button onclick="javascript:alert(0)">Load todo</button>


后面的代码给常简单。我们首先通过更新 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。你现在可以开始在你的应用中使用 mobxmobx-react 包啦。下面对你目前学到的东西做一个简要总结:

  1. 使用 @observable 装饰器或 observable(object or array) 函数使 MobX 可以追踪对象。
  2. @computed 装饰器可被用于创建基于 state 自动计算值的函数。
  3. 使用 autorun 来自动地运行依赖于 observable state 的函数。这对于打日志、发起网络请求等来说很有用。
  4. 使用 mobx-react 包中的 @observer 装饰器将你的 React 组件变得真正的可响应。他们将会自动并有效地更新。即使是在用够大量数据的大型复杂项目中。

多花点时间玩玩上面的可编辑代码块,以对 MobX 如何对你的操作作出响应有一个基本的概念。例如,你可以为 report 函数增加一个 log 语句来看它什么时候被调用;或者完全不要显示 report 来看看会对 TodoList 的渲染造成什么影响;或者在某些情况下不要显示它……

MobX 不是一个 state 容器

人们通常将 MobX 当做是 Redux 的替代。但是请注意,MobX 只是一个解决技术问题的库,其本身并没有 state 容器。从这个意义上说,上面的例子是人为设计的,所以我们建议您使用适当的工程实践,如在方法中封装逻辑、在 store 或控制器中组织它们等等。或者,像 HackerNews 中某位用户说的:

“MobX,它总是被提起,但我忍不住要赞美它。使用 MobX 写东西意味着它可以完成所有控制器(controllers) / 调度员(dispatchers) / 操作(actions) / 管理程序(supervisors) 以及其他管理数据流需要考虑的工作,而不只是完成一个 Todo 应用默认要求完成的那些工作。”