chenxiaochun / blog

🖋️ChenXiaoChun's blog
181 stars 15 forks source link

十分钟搞明白 MobX + React 使用教程 #51

Open chenxiaochun opened 6 years ago

chenxiaochun commented 6 years ago

MobX 是一个简单的、可扩展的、经过测试的状态管理解决方案。本教程将在十分钟内教给您介绍 MobX 的所有重要概念。

核心概念

状态是每个应用程序的核心。生成了不一致的状态或状态与本地变量的状态不同步,会造成应用程序很不容易管理,极易产生 bug。因此,许多状态管理解决方案试图限制你修改状态的方式,比如使用不可变的 state。但这会带来了一些新的问题,比如数据必须规范化,完整性约束失效等。

为了解决这些根本问题,MobX 再次使状态管理变得简单:使用它不可能产生不一致的状态。而且实现这一目标的策略很简单,就是确保一切都是从应用程序状态中自动派生出的。

MobX 处理你的应用程序状态如下图所示:

image

  1. 首先有一个 state,它可以是一个 object、array、primitives 等任何组成你程序的部分,你可以把这个想象成你应用程序的“单元格”。

  2. 其次就是 derivations,它一般是指可以从 state 中直接计算出来的结果。比如未完成的任务数量,也可以是稍微复杂的任务,比如渲染 html。你可以把它想象成电子表格中的“公式和图表”。

  3. Reactions 和 derivations 很像,主要的区别在于 reactions 并不产生数据结果,而是自动完成一些任务,一般是和 I/O 相关的。它们可以确保 DOM 更新和网络请求在正确的时间被自动执行。

  4. 最后是 actions。Actions 可以改变 state 的一切,MobX 会确保所有引起应用程序状态的更改都有对应的 derivations 和 reactions 相伴,保证同步。

一个简单的 todo store

看完上面的理论,还是应该用实际例子说明一下会更直白。我们从一个简单的 todo store 开始,它维护了一个待办事项集合,我们最开始先不用 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.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());

我们创建了一个TodoStore的实例,为了能看到每次的执行结果,每次在往TodoStore里添加了一条数据之后,都会调用一下report()方法。codepen 完整实例

执行结果也和我们预想的一样,每次都输出了当前的应用程序数据集合状态结果:

"Next todo: 'read MobX tutorial'. Progress: 0/1"
"Next todo: 'read MobX tutorial'. Progress: 0/2"
"Next todo: 'read MobX tutorial'. Progress: 1/2"
"Next todo: 'read MobX tutorial'. Progress: 1/2"
"Next todo: 'grok MobX tutorial'. Progress: 1/2"

如你所见,每次数据有变动时,都要手动去调用一下report()方法是一件很麻烦的事情。设想一下,能不能像 Excel 表格一样,当某个表格的数据有变动时,图表就能自动重新计算显示结果呢。没错,这就是 MobX 要解决的问题。

变成响应式

为了实现这个目标,需要把代码进行一下改动,使todos成为一个可观测(observable)的属性。

class ObservableTodoStore {
    @mobx.observable todos = [];
    @mobx.observable pendingRequests = 0;

    constructor() {
        mobx.autorun(() => console.log(this.report));
    }

    @mobx.computed get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }

    @mobx.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();

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()方法了。completedTodosCount会被自动从 todo list 中派生出来,使用@observable@computed可以使对象变成一个可观测的属性。

constructor中,使用autorun()包裹了一个输出report的函数,

codepen 完整示例

使 React 变成响应式

现在report方法已经变成了傻瓜式的响应。是时候围绕着此 store 来搭建用户界面了。React 组件默认并不是响应式的。使用mobx-react模块中的@observer修饰器将 React 组件包裹起来,以达到 render 方法能够自动运行,自动同步组件内的 state 状态。这个和上面repoet方法的实现概念是不一样的。

下面定义了一些 React 组件,其中仅用到了mobx-react@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')
);                  

codepen 完整示例

使用引用类型

到目前为止,我们已经创建过了可观测的对象、数组和基本类型。你可能会疑惑,在 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。一个存储的是人名,另一个存储的是 todo 列表。

异步 action

在我们这个小小的 todo 应用中,每一件事情都是由状态衍生出来的。这使得创建异步的 action 变得非常简单,

参考链接

wangwenyue commented 5 years ago

codepen 的例子都不能使用了,error:

index.js: Decorators are not officially supported yet in 6.x pending a proposal update.

chenxiaochun commented 5 years ago

教程还没有写完,感谢反馈。我稍后看看。😅

chenxiaochun commented 5 years ago

@wangwenyue ,示例已经更新,现在可以跑起来了。

wangwenyue commented 5 years ago

@wangwenyue ,示例已经更新,现在可以跑起来了。

Thanks, bro.

EasyChris commented 5 years ago

mobx-demo02 mobx-demo03 好像又挂了。。

chenxiaochun commented 5 years ago

@EasyChris ,问题已经修复。

ttthing111 commented 5 years ago

蹲后续啊

chenxiaochun commented 5 years ago

@ttthing111 ,有时间我会尽量更新哈。另外,刚刚看了下官网,发现它的好多 demo 都跑不起来了。😂