面向 React/Redux 开发者的 MobX 4 简介 示例 A1:React + MobX 示例 A2:那又怎样?示例 A3:但这不是面向对象 示例 B1:但我讨厌 prop 钻取!示例 B2:好吧,但异步怎么样?概念突破!是时候回顾一下了!示例 E1:MobX 开发工具 敬请期待……

2025-06-07

面向 React/Redux 开发者的 MobX 4 简介

示例 A1:React + MobX

示例 A2:那又怎样?

示例 A3:但这不是 OO

示例 B1:但是我讨厌螺旋桨钻孔!

示例 B2:好的,但是异步怎么样

概念突破!回顾一下!

示例 E1:MobX 开发工具

敬请关注...

MobX 利用可观察对象的“魔力”来管理状态和副作用。这不仅学习起来比较费劲,而且是一种完全不同的编程范式。目前关于如何将 React 与 Mobx 结合使用的最新培训材料并不多,而关于将 React 与 Redux 结合使用的内容却非常多。

在本介绍中,我们将逐步构建一个简单的应用程序,ping 一个模拟 API,以了解 MobX 如何与 React 协同工作,然后制作一个 MobX + React 看板来展示 MobX 的强大功能!

它看起来像这样

我们将如何进行:

  • 示例 A。构建一个基本应用,允许输入文本显示在Display中。我们展示了如何建立s 和组件的基础知识observableobserver
  • 示例 B 。我们将输入显示拆分为兄弟组件,以模拟更复杂的应用。我们还通过 ping 模拟 API 引入了异步状态更新。为此,我们使用mobx-react Provider将 MobX 状态放入 React 上下文中,以演示类似于 的兄弟组件间或兄弟组件间父组件间的简单通信react-redux
  • 示例 C :我们向应用添加了辅助显示。演示了变量(Mobx 概念)的实用性。computed
  • 示例 D:我们将应用扩展至任意数量的显示。演示了如何使用数组和映射来实现 MobX 状态。
  • 示例 E:调整和清理!我们添加了 MobX 开发工具,将整个应用程序置于模式中,并解释了 MobX s 和suseStrict的正式用法,以提高应用程序性能。actiontransaction

本教程将使用最近发布的 MobX 4和 MobX-React 5。很多人将 MobX 与装饰器联系在一起,而装饰器目前还处于第二阶段的提案阶段。这(理所当然地)会让一些人犹豫不决,但 MobX 4 引入了基于非装饰器的语法,让我们不再有这样的借口!然而,对于教程作者来说,这是一个问题,因为你必须决定是教授其中一种,还是两种都教授。为了解决这个问题,本教程中的每个示例都将使用非装饰器语法作为主要版本,但会提供一个使用装饰器的副本来展示等效的实现(例如,示例 A 与装饰器 A)。

读者须知:本文并非推荐 MobX 优于 Redux,反之亦然。本文旨在为像我这样只熟悉 Redux 的读者,如实地介绍 MobX 的核心概念。我会尝试得出一些结论,但理性的人可能会不同意。此外,Michel Weststrate 也曾多次强调,这两个库所针对的需求和价值完全不同

示例 A1:React + MobX

这是我们使用 React + MobX 的最基本的应用程序:

import { decorate, observable } from "mobx";
import { observer } from "mobx-react";

const App = observer(
  class App extends React.Component {
    text = ""; // observable state
    render() {
      // reaction
      return (
        <div>
          Display: {this.text} <br />
          <input
            type="text"
            onChange={e => {
              this.text = e.target.value; // action
            }}
          />
        </div>
      );
    }
  }
);
decorate(App, { text: observable });
Enter fullscreen mode Exit fullscreen mode

示例 A1装饰器 A1

您可以在此处看到observer连接可观察text属性,App以便每当您更新时它都会重新渲染text

虽然这很好,但它实际上与使用state和没有什么不同setState。如果你有 React,你不需要 MobX 来实现这一点。

示例 A2:那又怎样?

让我们尝试分离状态和视图模型的关注点:

// this deals with state
const appState = observable({
  text: "" // observable state
});
appState.onChange = function(e) { // action
  appState.text = e.target.value;
};

// this deals with view
const App = observer(
  class App extends React.Component {
    render() { // reaction
      const { text, onChange } = this.props.store;
      return (
        <div>
          Display: {text} <br />
          <input type="text" onChange={onChange} />
        </div>
      );
    }
  }
);

// you only connect state and view later on...
// ... 
<App store={appState} />
Enter fullscreen mode Exit fullscreen mode

示例 A2装饰器 A2

这里是store

  • 明确地作为 prop 传入(我们Provider稍后会使用该模式)
  • 自带动作处理程序(无需导入单独的 Reducer)

示例 A3:但这不是 OO

看一下上面代码的这部分。

const appState = observable({
  text: "" // observable state
});
appState.onChange = function(e) { // action
  appState.text = e.target.value;
};
Enter fullscreen mode Exit fullscreen mode

是的,我不喜欢这样。该方法没有封装在可观察对象中。我们可以让它更面向对象吗?

// import { decorate } from 'mobx'

class State {
  text = ""; // observable state
  onChange = e => (this.text = e.target.value); // action
};
decorate(State, { text: observable });
const appState = new State()
Enter fullscreen mode Exit fullscreen mode

示例 A3装饰器 A3

啊,好多了(尤其是装饰器示例,您不需要使用decorate)!

示例 B1:但是我讨厌螺旋桨钻孔!

就像react-redux可以将商店放入 一样Provider,它mobx-react也有一个Provider以相同方式工作的 。我们将 Display 和 Input 组件重构为兄弟应用程序:


import { inject, observer, Provider } from "mobx-react";

class State {
  text = ""; // observable state
  onChange = e => (this.text = e.target.value); // action
}
decorate(State, { text: observable });
const appState = new State();

const Display = inject(["store"])(
  observer(({ store }) => <div>Display: {store.text}</div>)
);

const Input = inject(["store"])(
  observer(
    class Input extends React.Component {
      render() {
        // reaction
        return <input type="text" onChange={this.props.store.onChange} />;
      }
    }
  )
);

// look ma, no props
const App = () => (
  <React.Fragment>
    <Display />
    <Input />
  </React.Fragment>
);

// connecting state with context with a Provider later on...
// ...
<Provider store={appState}>
    <App />
  </Provider>
Enter fullscreen mode Exit fullscreen mode

示例 B1装饰器 B1

请注意,如果我要添加第二个商店,我只需定义另一个observable,并将其作为Provider另一个 prop 传递给它,然后我就可以从任何子级调用它。不再是 redux 风格了combineReducers

使用提供程序还有助于避免创建全局存储实例,这是MobX React 最佳实践中强烈建议不要做的事情

MobX 4 注意:如果您只是尝试使用旧的 MobXobserver(['store'])简写(它始终与observer+同义inject(['store'])),您将收到一个非常好的弃用警告,告诉您不要再这样做了。

我发现这个注入/观察器语法有点繁琐,所以这是一个很好的小实用函数,你可以定义它来输入更少的内容:

const connect = str => Comp => inject([str])(observer(Comp));
Enter fullscreen mode Exit fullscreen mode

connect嘿!这就像我们来自 的好朋友react-redux!API 略有不同,但你可以随意定义🤷🏼‍♂️。

示例 B2:好的,但是异步怎么样

对于异步 API 获取,我们有几个选择。我们可以选择:

  • mobx-thunk
  • mobx-observable
  • mobx-saga
  • 以及大约 300 个其他选项。

它们都是特别的雪花,我们迫不及待地想看看你会做出什么决定!

暂停愤怒退出...

好吧,如果你看不出来,那我开玩笑的。使用可观察对象意味着你可以“直接”改变可观察对象,你的下游状态就会做出相应的反应。你可能已经注意到,上面的代码示例我一直用// reaction// action和来注释// observable state,它们的意思和英语中通常的意思一样。我们稍后再讨论这个问题。

回到代码!假设我们现在有一个名为 的异步 API fetchAllCaps。它Promise基本上会在等待 1 秒后将你传递给它的任何文本首字母大写。所以这模拟了你想要执行的任何异步操作的基本请求-响应流程。让我们将它插入到我们目前的示例中!

class State {
  text = ""; // observable state
  onChange = e => {
    // action
    this.text = e.target.value;
    fetchAllCaps(e.target.value).then(val => (this.text = val));
  };
}
decorate(State, { text: observable });
const appState = new State();
Enter fullscreen mode Exit fullscreen mode

示例 B2装饰器 B2

嗯,那很容易吗?

请注意,这里我们对该属性使用了公共类字段第 2 阶段功能onChange,而不使用同样属于第 2 阶段的装饰器。我决定这样做是因为公共类字段在 React 中非常普遍(例如,它附带create-react-app),您可能已经设置了它,或者可以弄清楚如何在 Babel 中进行设置(如果需要)。


概念突破!回顾一下!

到目前为止,我们还没有讨论 MobX 的核心概念,因此它们如下:

  • 可观察状态
  • 行动
  • 推导(反应和计算值)

在上面的例子中,我们已经使用了可观察状态以及修改这些状态​​的已定义操作mobx-react,并且我们使用了@observer来帮助绑定 React 组件以响应状态变化。所以这是 4 个中的 3 个。我们来看看计算值吧?


示例 C:计算值

计算值本质上是没有副作用的反应。由于可观察对象默认是惰性的,因此 MobX 能够根据需要延迟计算。它们只会在可观察状态更新时才更新。换句话说,计算值源自可观察状态。

让我们添加一个计算值,它只是反转中的所有内容text

class State {
  text = "";
  get reverseText() {
    return this.text
      .split("")
      .reverse()
      .join("");
  }
  onChange = e => {
    // action
    this.text = e.target.value;
    fetchAllCaps(e.target.value).then(val => (this.text = val));
  };
}
decorate(State, { text: observable, reverseText: computed });
const appState = new State();

// lower down...
const Display2 = inject(["store"])(
  observer(({ store }) => <div>Display: {store.reverseText}</div>)
);
Enter fullscreen mode Exit fullscreen mode

示例 C1装饰器 C1

太棒了!它“真的有效”(TM)!

看到这里,你可能会想:为什么要这么做?我明明可以把同步业务逻辑放在 Reactrender函数里,为什么还要在 appState 层计算呢?

在这个小例子中,这样的批评还算合理,但想象一下,如果你在应用中的多个地方依赖相同的计算值,那会是怎样的体验?你得把相同的业务逻辑复制到各个地方,或者提取到一个文件,然后再导入到各个地方。计算值是建模状态派生的好方法,因为它更靠近状态,而不是靠近视图。这虽然只是细微的差别,但在规模化应用时却可能产生重大影响。

顺便说一句,vue.js也有计算变量,而Angular只是隐式地使用它们。

示例 D1:可观察数组

MobX 基本上可以让任何东西都可观察。我来引用一下文档

  1. 如果值是 ES6 Map,则将返回一个新的Observable Map。如果您不想只对特定条目的变化做出反应,还想对条目的添加或删除做出反应,那么 Observable Map 非常有用。
  2. 如果值是一个数组,则将返回一个新的Observable 数组。
  3. 如果 value 是一个没有原型的对象,则其所有当前属性都将变为可观察的。参见可观察对象
  4. 如果值是一个带有原型的对象、JavaScript 原语或函数,则会返回一个Boxed Observable。MobX不会自动将带有原型的对象设置为可观察对象;因为这是其构造函数的责任。请在构造函数中使用 extendsObservable,或在类定义中使用 @observable。

在上面的例子中,我们迄今为止一直在制作Boxed ObservablesObservable Objects,但如果我们想要制作一个可观察数组怎么办?

可观察数组类似数组的对象,而不是真正的数组。这可能会给用户带来麻烦,尤其是在将数据传递给其他库时。要转换为普通的 JS 数组,请调用observable.toJS()observable.slice()

但大多数时候,你可以直接将数组视为数组。这是一个使用可观察数组的非常简单的 Todo 应用:

class State {
  text = ["get milk"]; // observable array
  onSubmit = e => this.text.push(e); // action
}
decorate(State, { text: observable });
const appState = new State();

const Display = inject(["store"])(
  observer(({ store }) => (
    <ul>Todo: {store.text.map(text => <li key={text}>{text}</li>)}</ul>
  ))
);

const Input = observer(
  ["store"],
  class Input extends React.Component {
    render() {
      // reaction
      return (
        <form
          onSubmit={e => {
            e.preventDefault();
            this.props.store.onSubmit(this.input.value);
            this.input.value = "";
          }}
        >
          <input type="text" ref={x => (this.input = x)} />
        </form>
      );
    }
  }
);

const App = () => (
  <React.Fragment>
    <Display />
    <Input />
  </React.Fragment>
);
Enter fullscreen mode Exit fullscreen mode

示例 D1装饰器 D1

请注意,“just push” 才有效!

示例 D2:可观察映射

可观察对象(我们在示例 A、B 和 C 中使用的对象)和可观察映射(Observable Maps)之间有什么区别?嗯,这和普通的旧式 JavaScript 对象(Plain Old JavaScript Objects)和ES6 映射(Maps)之间的区别是一样的。我将引用 MobX 文档来解释何时使用映射(Maps)而不是对象(Objects):

如果您不想仅对特定条目的变化做出反应,而且还想对条目的添加或删除做出反应,那么可观察的地图非常有用。

所以,如果我们想要一堆待办事项列表,并且可以在其中添加新的待办事项,这就是正确的抽象。因此,如果我们从示例 D1 中取出那个应用程序,将其重命名为 ,TodoList并进行todolist.js一些其他简单的调整,那么index.js我们可以这样做:

// index.js
const connect = str => Comp => inject([str])(observer(Comp)); // helper function

const listOfLists = observable.map({
  Todo1: new TodoListClass(),
  Todo2: new TodoListClass()
  // observable map rerenders when you add new members
});
const addNewList = e => listOfLists.set(e, new TodoListClass());

const App = connect("lists")(
  class App extends React.Component {
    render() {
      const { lists } = this.props;
      return (
        <div className="App">
          <span />
          <h1>MobX Kanban</h1>
          <span />
          {Array.from(lists).map((k, i) => (
            <div key={i}>
              {/*Provider within a Provider = Providerception */}
              <Provider todolist={k}>
                <TodoList />
              </Provider>
            </div>
          ))}
          <div>
            <h3>Add New List</h3>
            <form
              onSubmit={e => {
                e.preventDefault();
                addNewList(this.input.value);
                this.input.value = "";
              }}
            >
              <input type="text" ref={x => (this.input = x)} />
            </form>
          </div>
        </div>
      );
    }
  }
);
Enter fullscreen mode Exit fullscreen mode

示例 D2装饰器 D2

瞧!我们有了看板(一个可扩展的列表列表)!

看板

这是通过 Observable Map 的动态扩展功能实现的listOfLists。说实话,你可能也可以使用数组来实现这一点,但如果你有更适合演示 Observable Map 的用例,请在下面的评论中告诉我。

示例 E1:MobX 开发工具

Redux 开发工具(理所当然地)是 Redux 价值的重要组成部分,所以让我们来看看MobX React 开发工具吧!

import DevTools from 'mobx-react-devtools'; // npm install --save-dev mobx-react-devtools

// somewhere within your app...
        <DevTools />
Enter fullscreen mode Exit fullscreen mode

示例 E1装饰器 E1

您可以看到弹出三个图标:

  • 可视化重新渲染
  • 审计依赖关系树
  • 将所有内容记录到控制台(使用浏览器控制台而不是 Codepen 控制台)

虽然您无法进行时间旅行,但这是一套非常好的工具,可以审核应用程序中发生的任何意外状态变化。

敬请关注...

mobx-dev-tools和4有一个阻塞错误mobxhttps://github.com/mobxjs/mobx-react-devtools/issues/86,当错误修复后,我将完成此操作。

然而与此同时,你可以看看如何明确定义,actions以便 MobX 可以批量处理你的状态变化transaction,这可以大大节省性能:

https://mobx.js.org/refguide/action.html

请注意,我们无需使用 s 就能完成所有演示action- MobX 有一个(记录不充分的)严格模式(以前​​是useStrict,现在是configure({enforceActions: true});) - 请参阅MobX 4 文档。但我们需要开发工具来真正展示示例应用的优势。

致谢

本介绍借鉴了Michel Weststrate 的 egghead.io 课程的大量代码和结构,但更新了这门已有两年历史的课程,使其适用于当前的 Mobx 4 API。我还要感谢我的雇主允许我公开学习。

这里的例子是在Javid AskerovNader DabitMichel的帮助下完成的

其他教程和进一步阅读

其他近期指南

文档

较旧

相关库探索

贡献

我还应该在本指南中包含哪些现有(不到一年)的资源?我犯了什么错误吗?请在下方留言告诉我!

文章来源:https://dev.to/swyx/introduction-to-mobx-4-for-reactredux-developers-3k07
PREV
My 2021 New Mac Setup
NEXT
如果你在网上卖任何东西,这本书都能帮你赚钱