八、探索 mobx utils 和 mobx 状态树

当您开始深入了解 MobX 世界时,您将意识到某些类型的用例经常重复出现。第一次解决这些问题时,一定会有成就感。但是,在第五次之后,您需要标准化解决方案。mobx-utils是一个 NPM 包,它为您提供了几个标准实用程序来处理 MobX 中的常见用例。

为了进一步提高标准化水平,我们可以将更多结构化的观点引入到我们的 MobX 解决方案中。这些观点是在 MobX 使用数年后形成的,并承载了各种快速发展的想法。这在mobx-state-treeNPM 包中都是可能的。

在本章中,我们将更详细地介绍以下程序包:

  • mobx-utils用于具有实用功能的工具带
  • mobx-state-treeMST)针对固执己见的 MobX

技术要求

您需要在系统上安装 Node.js。最后,要使用本书的 Git 存储库,用户需要安装 Git。

本章代码文件可在 GitHub 上找到: https://github.com/PacktPublishing/Mobx-Quick-Start-Guide/tree/master/src/Chapter08

查看以下视频以查看代码的运行: http://bit.ly/2LiFSJO

mobx-utils 的效用函数

mobx-utils提供了多种实用功能,可以简化 MobX 中的编程任务。您可以使用npmyarn安装mobx-utils

$ npm install mobx-utils

在本节的其余部分中,我们将重点介绍一些常用的实用程序。这些措施包括:

  • fromPromise()
  • lazyObservable()
  • fromResource()
  • now()
  • createViewModel()

使用 fromPromise()可视化异步操作

承诺是 JavaScript 中的一种生活方式,非常适合处理异步操作。在 React UI 上表示操作状态时,我们必须确保承诺的三种状态都得到了处理。这包括承诺为pending(操作进行中)、fulfilled(操作成功完成)或rejected(如果失败)时的状态。fromPromise()是一种处理承诺的便捷方式,并提供了一个很好的 API 来直观地表示三种状态:

newPromise = fromPromise(promiseLike)

promiseLikePromise(resolve, reject) => { }的实例

fromPromise()包装给定的承诺,并返回一个新的、带有 MobX 电荷的承诺,该承诺具有一些额外的可观察属性:

  • state:三个字符串值之一:pendingfulfilledrejected:它们在mobx-utilsmobxUtils.PENDINGmobxUtils.FULFILLEDmobxUtils.REJECTED中也可以作为常量提供。
  • value:已解决的valuerejected错误。使用state区分数值。
  • case({pending, fulfilled, rejected}):提供三种状态的反应组分。

让我们通过一个例子来了解所有这些。我们将创建一个简单的Worker类来执行一些操作,这些操作可能会随机失败。下面是通过调用fromPromise()来跟踪操作的Worker类。请注意,我们正在将一个promise作为参数传递到fromPromise()

import { fromPromise, PENDING, FULFILLED, REJECTED } from 'mobx-utils';
class Worker {
    operation = null;
    start() {
 this.operation = fromPromise(this.performOperation());
    }
    performOperation() {
        return new Promise((resolve, reject) => {
            const timeoutId = setTimeout(() => {
                clearTimeout(timeoutId);
                Math.random() > 0.25 
                    ? resolve('200 OK') 
                    : reject(new Error('500 FAIL'));
            }, 1000);
        });
    }
}

为了可视化这个操作,我们可以利用case()API 来显示每个状态对应的 React 组件。这可以在下面的代码中看到。当操作从pending进展到fulfilledrejected时,将使用正确的反应成分呈现这些状态。对于fulfilledrejected状态,已解析的valuerejected``error作为第一个参数传入:

import { fromPromise, PENDING, FULFILLED, REJECTED } from 'mobx-utils';
import { observer } from 'mobx-react';

import React, { Fragment } from 'react';
import { CircularProgress, Typography } from '@material-ui/core/es/index';

@observer export class FromPromiseExample extends React.Component {
    worker;

    constructor(props) {
        super(props);

 this.worker = new Worker();
 this.worker.start();
    }

    render() {
        const { operation } = this.worker;
 return operation.case({
            [PENDING]: () => (
                <Fragment>
                    <CircularProgress size={50} color={'primary'} />
                    <Typography variant={'title'}>
                        Operation in Progress
                    </Typography>
                </Fragment>
            ),
            [FULFILLED]: value => (
                <Typography variant={'title'} color={'primary'}>
                    Operation completed with result: {value}
                </Typography>
            ),
            [REJECTED]: error => (
                <Typography variant={'title'} color={'error'}>
                    Operation failed with error: {error.message}
                </Typography>
            ),
        });
    }
}

Instead of the case() function, we could have also switched manually on the observable state property. In fact, case() does that internally.

使用 lazyObservable()进行延迟更新

对于执行成本较高的操作,将其推迟到需要时进行是有意义的。通过lazyObservable(),您可以跟踪这些操作的结果,并仅在需要时更新。它接受一个执行计算的函数,并在准备就绪时推送值:

result = lazyObservable(sink => { }, initialValue)

这里,sink是要调用的回调,用于将值推送到lazyObservable上。懒惰的可观察对象也可以从一些initialValue开始。

可以使用result.current()检索lazyObservable()的当前值。一旦更新了懒惰的可观察对象,result.current()将有一些值。要再次更新懒惰的可观察对象,可以使用result.refresh()。这将重新调用计算,并最终通过sink回调推送新值。请注意,sink回调可以根据需要多次调用。

在下面的代码段中,您可以看到使用lazyObservable()来更新操作的值:

import { lazyObservable } from 'mobx-utils';

class ExpensiveWorker {
    operation = null;

    constructor() {
 this.operation = lazyObservable(async sink => {
 sink(null); // push an empty value before the update
            const result = await this.performOperation();
 sink(result);
        });
    }

    performOperation() {
        return new Promise(resolve => {
            const timeoutId = setTimeout(() => {
                clearTimeout(timeoutId);
                resolve('200 OK');
            }, 1000);
        });
    }
}

The call to the current() method is tracked by MobX, so make sure you only call it when needed. The use of this method inside render() causes MobX to re-render the component. After all, render() of a component translates to a reaction in MobX, which re-evaluates whenever any of its tracked observables change.

为了在 React 组件(一个观察者中使用惰性可观察物,我们依赖current()方法获取其值。MobX 将跟踪该值,并在组件发生更改时重新渲染该组件。请注意,在按钮的onClick处理程序中,我们通过调用其refresh()方法来更新惰性可观察对象:

import { observer } from 'mobx-react';
import React, { Fragment } from 'react';
import {
    Button,
    CircularProgress,
    Typography,
} from '@material-ui/core/es/index';
 @observer
export class LazyObservableExample extends React.Component {
    worker;
    constructor(props) {
        super(props);

 this.worker = new ExpensiveWorker();
    }
   render() {
 const { operation } = this.worker;
 const result = operation.current();
        if (!result) {
            return (
                <Fragment>
                    <CircularProgress size={50} color={'primary'} />
                    <Typography variant={'title'}>
                        Operation in Progress
                    </Typography>
                </Fragment>
            );
        }
         return (
            <Fragment>
                <Typography variant={'title'} color={'primary'}>
                    Operation completed with result: {result}
                </Typography>
                <Button
                    variant={'raised'}
                    color={'primary'}
 onClick={() => operation.refresh()}                >
                    Redo Operation
                </Button>
            </Fragment>
        );
    }
}

具有 fromResource()的广义 lazyObservable()

还有一种更广义的形式lazyObservable()称为fromResource()。与lazyResource()类似,它接受一个带有sink回调的函数。它充当一个订阅函数,只有在实际请求资源时才会调用该函数。此外,它还需要第二个参数,即取消订阅函数,该函数可用于在不再需要资源时进行清理:

resource = fromResource(subscriber: sink => {}, unsubscriber: () => {},    
           initialValue)

fromResource()返回一个 observable,该 observable 将在第一次调用其current()方法时开始获取值。它会返回一个可观测值,该可观测值还具有停止更新值的dispose()方法。

在下面的代码片段中,您可以看到一个依赖于fromResource()来管理其 WebSocket 连接的DataService类。数据值可通过data.current()检索。在这里,数据充当懒惰的可观察对象。在订阅功能中,我们设置我们的 WebSocket 并订阅特定频道。我们在fromResource()退订功能中退订该频道:

import { fromResource } from 'mobx-utils';

class DataService {
    data = null;
    socket = null;

    constructor() {
 this.data = fromResource(
            async sink => {
                this.socket = new WebSocketConnection();
                await this.socket.subscribe('data');

                const result = await this.socket.get();

                sink(result);
            },
            () => {
                this.socket.unsubscribe('data');
                this.socket = null;
            },
        );
    }
}

const service = new DataService();
console.log(service.data.current());

// After some time, when no longer needed
service.data.dispose();

我们可以使用dispose()方法显式地处理资源。但是,MobX 足够聪明,可以知道何时不再有此资源的观察者,并自动调用取消订阅函数。

A special kind of lazy-observable provided by mobx-utils is now(interval: number). It treats time as an observable and updates at the given interval. You can retrieve its value by simply calling now(), which, by default, updates every second. By the virtue of being an observable, it will also cause any reaction to execute every second. Internally, now() uses the fromResource() utility to manage the timer.

用于管理编辑的视图模型

在基于数据输入的应用中,很常见的情况是使用表单来接受各种字段。在这些表单中,在用户提交表单之前,原始模型不会发生变化。这允许用户取消编辑过程并返回到以前的值。这样的场景需要创建原始模型的克隆,并在提交时推送编辑。虽然这项技术并不十分复杂,但它确实添加了一些样板。

mobx-utils提供了一个名为createViewModel()的便捷实用程序,该实用程序专为此场景量身定制:

viewModel = createViewModel(model)

model是包含可观察属性的原始模型。createViewModel()包装此模型并代理所有读写操作。此实用程序具有一些有趣的特性,如下所示:

  • 只要不改变viewModel的属性,它将返回原始模型的值。更改后,它将返回更新后的值,并将viewModel视为脏。
  • 要最终确定原始模型上的更新值,必须调用viewModelsubmit()方法。若要反转任何更改,可以调用reset()方法。要还原单个属性,请使用resetProperty(propertyName: string)
  • 要检查viewModel是否脏,请使用isDirty属性。要检查单个属性是否脏,请使用isPropertyDirty(propertyName: string)
  • 要获得原始模型,请使用简便的model()方法。

使用createViewModel()的优点是,您可以将整个编辑过程视为一个事务。只有在submit()被调用时才是最终的。这允许您提前取消并将原始模型保留在其以前的状态。

在下面的示例中,我们正在创建一个包装FormData实例并记录viewModelmodel属性的viewModel。您将注意到viewModel的代理效应,以及值如何在submit()时传播回模型:

class FormData {
    @observable name = '<Unnamed>';
    @observable email = '';
    @observable favoriteColor = '';
}

const viewModel = createViewModel(new FormData());

autorun(() => {
    console.log(
        `ViewModel: ${viewModel.name}, Model: ${
            viewModel.model.name
        }, Dirty: ${viewModel.isDirty}`,
    );
});

viewModel.name = 'Pavan';
viewModel.email = 'pavan@pixelingene.com';
viewModel.favoriteColor = 'orange';

console.log('About to reset');
viewModel.reset();

viewModel.name = 'MobX';

console.log('About to submit');
viewModel.submit();

autorun()的日志如下。您可以看到submit()reset()viewModel.name属性的影响:

ViewModel: <Unnamed>, Model: <Unnamed>, Dirty: false
ViewModel: Pavan, Model: <Unnamed>, Dirty: true
About to reset...
ViewModel: <Unnamed>, Model: <Unnamed>, Dirty: false
ViewModel: MobX, Model: <Unnamed>, Dirty: true
About to submit...
ViewModel: MobX, Model: MobX, Dirty: false

还有很多东西要发现

这里描述的少数实用程序并非详尽无遗。mobx-utils提供了更多的实用程序,我们强烈建议您看看 GitHub 项目(https://github.com/mobxjs/mobx-utils 来发现剩余的实用功能。

有在 RxJS 流和 MobX 可观测数据之间进行转换的函数、处理器函数可以在添加可观测数组时执行操作、MobXwhen()的变体(超时后自动处理)等等。

具有 MobX 状态树的自以为是 MobX

MobX 在组织状态和应用各种动作和反应方面非常灵活。但是,它确实给您留下了一些问题需要回答:

  • 应该使用类还是只使用带extendObservable()的普通对象?
  • 数据应该如何标准化?
  • 序列化状态时如何处理循环引用?
  • 还有更多

mobx-state-tree是一个为你组织和构建你的可观察状态提供规定性指导的软件包。采用 MST 思维方式会给您带来一些开箱即用的好处。在本节中,我们将探讨此包及其好处。

模型–属性、视图和操作

mobx-state-tree顾名思义,在模型树中组织状态。这是一种模型优先的方法,其中每个模型定义了需要捕获的状态。定义模型增加了在运行时对模型分配进行类型检查并防止意外更改的能力。将运行时检查与诸如 TypeScript 之类的语言的使用相结合,还可以获得编译时(或者更确切地说,设计时)类型安全性。使用严格类型化模型,mobx-state-tree为您提供安全保证,并确保类型化模型的完整性和约束。这本身就是一个巨大的好处,尤其是在处理 JavaScript 这样的动态语言时。

让我们用Todo的一个简单模型将 MST 付诸实施:

import { types } from 'mobx-state-tree';

const Todo = types.model('Todo', {
    title: types.string,
    done: false,
});

模型描述它所持有的数据的形状。对于Todo模型,它只需要一个title字符串和一个布尔值done属性。请注意,我们为模型指定了一个大写的名称(Todo。这是因为 MST 真正定义的是类型而不是实例。

MST 中的所有内置类型都是types命名空间的一部分。types.model()方法有两个参数:可选字符串名称(用于调试和错误报告)和定义类型各种属性的对象。所有这些属性都将使用严格类型进行限定。让我们尝试创建此模型的实例:

const todo = Todo.create({
    title: 'Read a book',
    done: false,
});

请注意,我们是如何将相同的数据结构传递到模型中定义的Todo.create()中的。传递任何其他类型的数据将导致 MST 抛出类型错误。创建模型的实例也使其所有属性成为可观察的。这意味着我们现在可以充分利用 mobxapi 的强大功能

让我们创建一个简单的反应来记录对todo实例的更改:

import { autorun } from 'mobx';

autorun(() => {
    console.log(`${todo.title}: ${todo.done}`);
});

// Toggle the done flag
todo.done = !todo.done; 

如果运行此代码,您将注意到抛出了一个异常,如下所示:

Error: [mobx-state-tree] Cannot modify 'Todo@<root>', the object is protected and can only be modified by using an action.

发生这种情况是因为我们在操作之外修改了todo.done属性。你会从前面的章节中回忆起,将所有可观察到的突变封装在一个动作中是一个很好的实践。事实上,甚至有一个 mobxapi:configure({ enforceActions: 'strict' }),以确保实现这一点。MST 对其状态树中的数据非常保护,并要求对所有突变使用操作。

这听起来可能很僵硬,但它确实带来了额外的好处。例如,操作的使用允许 MST 为中间件提供一流的支持。中间件可以截获状态树发生的任何变化,使得实现日志记录、时间旅行撤销/重做数据库同步等功能变得简单。

定义模型上的操作

我们之前创建的模型类型Todo可以通过链式 API 进行扩展。actions()就是这样一个 API,可以用它来扩展所有动作定义的模型类型。让我们为我们的Todo类型这样做:

const Todo = types
    .model('Todo', {
        title: types.string,
        done: false,
    })
 .actions(self => ({
 toggle() {
 self.done = !self.done;
 },
 }));

const todo = Todo.create({
    title: 'Read a book',
    done: false,
});

autorun(() => {
    console.log(`${todo.title}: ${todo.done}`);
});

todo.toggle();

actions()方法接受一个函数,该函数接收模型实例作为其参数。在这里,我们称之为self。此函数应返回定义所有操作的键值映射。在前面的代码片段中,我们利用 ES2015 的对象文字语法使 actions 对象看起来更可读。这种接受行为的方式有一些显著的好处:

  • 使用函数可以创建一个闭包,该闭包可用于跟踪仅由操作使用的私有状态。例如,在某个操作中设置的 WebSocket 连接不应暴露于外部世界。
  • 通过将模型实例传递给actions(),可以保证this指针始终正确。您再也不用担心actions()中定义的函数的上下文了。toggle()操作使用self来变异模型实例。

定义的动作可以直接在模型实例上调用,这就是我们对todo.toggle()所做的。MST 不再抱怨直接突变,当todo.done发生变化时autorun()也会触发。

使用视图创建派生信息

与 actions 类似,我们也可以使用views()扩展模型类型。模型中的衍生信息使用 MST 中的views()定义。与actions()方法一样,可以链接到模型类型:

const Todo = types
    .model(/* ... */)
    .actions(/* ... */)
 .views(self => ({
 get asMarkdown() {
 return self.done
                ? `* [x] ~~${self.title}~~`
                : `* [ ] ${self.title}`;
 },

 contains(text) {
 return self.title.indexOf(text) !== -1;
 },
 })); const todo = Todo.create({
    title: 'Read a book',
    done: false,
});

autorun(() => {
    console.log(`Title contains "book"?: ${todo.contains('book')}`);
});

console.log(todo.asMarkdown);
// * [ ] Read a book

console.log(todo.contains('book')); // true

Todo类型介绍了两种观点:

  • asMarkdown()是一个getter转换为 MobX 计算属性。与每个计算属性一样,其输出也是缓存的。
  • contains()是一个常规函数,其输出不缓存。但是,当在reaction()autorun()等被动上下文中使用时,它确实能够重新执行。

mobx-state-tree引入了非常严格的模型概念,明确定义了状态动作派生。如果您对在 MobX 中构建代码感到不确定,MST 可以帮助您在明确的指导下应用 MobX 哲学。

微调原语类型

到目前为止,我们看到的单一模型类型只是一个开始,几乎不能称为树。我们可以扩展域模型,使其更真实。让我们添加一个User类型,他将创建todo项:

import { types } from 'mobx-state-tree';

const User = types.model('User', {
    name: types.string,
    age: 42,
    twitter: types.maybe(types.refinement(types.string, v => 
      /^\w+$/.test(v))),
});

上述定义中有一些有趣的细节,如下所示:

  • age属性已定义为常量42,它转换为age的默认值。当没有为用户提供值时,它将被设置为此默认值。此外,MST 足够聪明,可以将类型派生为number。这适用于所有基本类型,其中默认值的类型将被推断为属性的类型。此外,通过提供默认值,我们建议age属性是可选的。更详细的属性声明形式是:types.optional(types.number, 42)
  • twitter属性的定义更为复杂,但很容易分解。types.maybe()表示twitter句柄是可选的,所以可能是未定义的。提供值时,该值必须为字符串类型。但不是任何字符串;仅限与提供的正则表达式匹配的字符串。这将为您提供运行时类型安全性,并拒绝无效的 Twitter 句柄,如Calvin & Hobbes或空字符串。

MST 提供的类型系统功能非常强大,可以处理各种复杂的类型规范。它也能很好地组合,并为您提供了一种将许多较小类型组合成较大类型定义的函数方法。这些类型规范为您提供了运行时安全性,并确保了域模型的完整性。

构树

现在我们有了TodoUser类型,我们可以定义顶层App类型,它构成了前面定义的类型。App类型表示应用的状态:

const App = types.model('App', {
 todos: types.array(Todo),
 users: types.map(User),
});

const app = App.create({
    todos: [
        { title: 'Write the chapter', done: false },
        { title: 'Review the chapter', done: false },
    ],
    users: {
        michel: {
            name: 'Michel Westrate',
            twitter: 'mwestrate',
        },
        pavan: {
            name: 'Pavan Podila',
            twitter: 'pavanpodila',
        },
    },
});

app.todos[0].toggle();

我们使用高阶类型(将一个类型作为输入并创建一个新类型的类型)定义了App类型。在前面的代码片段中,types.map()types.array()创建了这些高阶类型。

创建一个App类型的实例只是提供正确的 JSON 负载。只要结构与类型规范匹配,MST 在运行时构造模型实例时就不会有问题。

Remember: The shape of the data will always be validated by MST. It will never allow data updates that don't match the model's type specification.

请注意,在前面的代码片段中,我们可以无缝地调用app.todos[0].toggle()方法。这是因为 MST 能够成功构建app实例,并用适当的类型包装 JSON 节点。

mobx-state-tree elevates the importance of modeling your application state. Defining the proper types for the various entities in your application is paramount for its structural and data integrity. A nice way to get started is to encode the JSON you receive from the server in MST models. The next step is to fatten the model by adding more rigid typing, and attaching actions and views.

参考文献和标识符

到目前为止,本章完全是关于在中捕获应用的状态。树有许多有趣的特性,很容易理解和探索。但是,当一个人开始将一项新技术应用于实际问题领域时,往往发现树在概念上不足以描述问题领域。例如,友谊关系是双向的,不适合单向树。处理本质上不是组合,而是关联的关系,通常需要引入新的抽象层和技术,如数据规范化

在我们的应用中,可以通过给出Todo一个assignee属性来介绍这种关系的一个快速示例。现在,很明显,Todo并不拥有它的assignee,相反的情况也不成立;TODO不属于单个用户,因为它们可以在之后重新分配。因此,当组合不足以描述关系时,我们通常会使用外键来描述关系。

换句话说,Todo项的 JSON 可以像下面的代码一样,Todoassignee字段对应于User对象的userid字段:

Using name to store the assignee relationship would be a bad idea, as the name of a person is not unique and it might change over time.

{
    todos: [
        {
            title: 'Learn MST',
            done: false,
            assignee: '37',
        },
    ],
    users: {
        '37': {
            userid: '37',
            name: 'Michel Weststrate',
            age: 33,
            twitter: 'mweststrate',
        },
    },
}

我们对此的最初理解可能是将assigneeuserid属性键入types.string字段。然后,无论何时我们需要它,我们都可以在users地图中查找指定的用户,因为该用户存储在它自己的userid下。由于用户查找可能是一个常见的操作,我们甚至可以引入一个视图动作来读取或写入该用户。这将使我们的用户模型如以下代码所示:

import { types, getRoot } from 'mobx-state-tree';

const User = types.model('User', {
 userid: types.string, // uniquely identifies this User    name: types.string,
    age: 42,
    twitter: types.maybe(types.refinement(types.string, v => /^\w+$/.test(v))),
});

const Todo = types
    .model('Todo', {
 assignee: types.string, // represents a User        title: types.string,
        done: false,
    })
    .views(self => ({
 getAssignee() {
            if (!this.assignee) return undefined;
            return getRoot(self).users.get(this.assignee);
        },
    }))
    .actions(self => ({
 setAssignee(user) {
            if (typeof user === 'string') this.assignee = user;
            else if (User.is(user)) this.assignee = user.userid;
            else throw new Error('Not a valid user object or user id');
        },
    }));

const App = {
    /* as is */
};

const app = App.create(/* ... */);

console.log(app.todos[0].getAssignee().name); // Michel Weststrate

getAssignee()视图中,我们方便地使用每个 MST 节点都知道自己在树中的位置这一事实。通过利用getRoot()实用程序,我们可以导航到users地图并抓取正确的User对象。通过使用getAssignee()视图,我们获得了一个真实的User对象,以便我们可以直接访问并打印其name属性。

There are several useful utilities that can be used to reflect on or work with the location in a tree, such as getPath()getParent() , getParentOfType(), and so on. As an alternative, we could have expressed the getAssignee() view as return resolvePath(self, "../../users/" + self.assignee).

We can treat the MST tree as a filesystem for state! getAssignee() just translates to a symlink.

此外,还引入了更新assignee属性的操作。为了确保通过提供userid,或实际用户对象可以方便地调用setAssignee()动作,我们应用了一些类型判别。在 MST 中,每个类型不仅公开了create()方法,还公开了is方法,以检查给定值是否属于相应的类型。

通过 types.identifier()和 types.reference()引用

我们可以在 MST 中灵活地表达这些查找/更新实用程序,这很好,但如果您的问题域很大,这将成为一个相当重复的模式。幸运的是,此模式内置于 MST 中。我们可以利用的第一种类型是types.identifier(),它表示某个字段唯一地标识某个模型类型的实例。因此,在我们的示例中,我们可以将其键入为types.identifier(),而不是将userid键入为types.string

其次是types.reference()。此类型表示某个字段被序列化为基元值,但实际上表示对树中另一类型的引用。MST 将自动为我们匹配identifier字段和reference字段,因此我们可以将以前的状态树模型简化为以下内容:

import { types } from "mobx-state-tree"

const User = types.model("User", {
 userid: types.identifier(), // uniquely identifies this User
  name: types.string,
  age: 42,
  twitter: types.maybe(types.refinement(types.string, (v => /^\w+$/.test(v))))
})

const Todo = types.model("Todo", {
 assignee: types.maybe(types.reference(User)), // a Todo can be assigned to a User
  title: types.string,
  done: false
})

const App = /* as is */

const app = App.create(/* */)
console.log(app.todos[0].assignee.name) // Michel Weststrate

由于引用类型,读取Todoassignee属性实际上将解析存储的标识符并返回正确的User对象。因此,我们可以在前面的示例中立即打印它的名称。请注意,在幕后,我们的状态仍然是一棵树。还需要注意的是,我们不必指定在何处或如何解析对User实例的引用。MST 将自动维护一个基于内部类型+标识符的查找表,用于解析引用。通过使用引用标识符,MST 有足够的类型信息为我们自动处理数据(反)规范化

types.reference is quite powerful and can be customized to, for example, resolve objects based on relative paths (like a real symlink!) instead of identifiers. In many cases, you will combine types.reference with types.maybe as above, to express that Todo does not necessarily have an assignee. Likewise, arrays and maps of references can be modeled in similar ways.

声明性模型的开箱即用优势

MST 帮助您以声明的方式组织和建模复杂的问题域。由于在您的域中定义类型的方法是一致的,因此我们可以从干净简单的心智模型中获益。这种一致性还为我们提供了许多开箱即用的特性,因为 MST 对状态树有深入的了解。我们前面看到的一个例子是使用标识符引用进行自动数据规范化。MST 内置了更多的功能。其中,有几个最实用。我们将在本节的其余部分简要讨论这些问题。

不变快照

MST 总是在内存中保存状态树的不可变版本,可以使用getSnapshot()API 检索。本质上,const snapshot = getSnapshot(tree)const tree = Type.create(snapshot)相反。getSnapshot()使得快速序列化树的整个状态非常方便。由于 MST 由 MobX 提供动力,我们也可以很好地跟踪这一点。

Snapshots translate to computed-properties on the model instances. 

以下代码段在每次更改时自动将树的状态存储在本地存储中,但不超过每秒一次:

import { reaction } from 'mobx';
import { getSnapshot } from 'mobx-state-tree';

const app = App.create(/* as before */);

reaction(
    () => getSnapshot(app),
    snapshot => {
        window.localStorage.setItem('app', JSON.stringify(snapshot));
    },
    { delay: 1000 },
);

应该指出的是,MST 树中的每个节点本身就是一个 MST 树。这意味着,在根上调用的任何操作也可以在其任何子树上调用。例如,如果我们只想存储整个状态的一部分,我们可以只获取子树的快照。

A corollary API that goes hand in hand with getSnapshot() is applySnapshot(). This can be used to update a tree with a snapshot in an efficient manner. By combining getSnapshot() and applySnapshot(), you can build a time traveler in just a few lines of code! This is left as an exercise for the reader.

JSON 补丁

尽管快照可以有效地捕获整个应用的状态,但它们不适合与服务器或其他客户端进行频繁通信。这是因为快照的大小与要序列化的状态的大小成线性增长。相反,对于实时更改,最好将增量更新发送到服务器。JSON 修补程序(RFC-6902)是关于如何序列化这些增量更新的官方标准,MST 支持这种开箱即用的标准。

onPatch()API 可用于监听patches作为您的更改的副作用而生成。另一方面,applyPatch()执行相反的过程:给定补丁,它可以更新现有的树。onPatch()侦听器发出由动作所做的状态更改产生的patches。它还公开了所谓的反向补丁:一组可以撤销patches所做更改的补丁:

import { onPatch } from 'mobx-state-tree';

const app = App.create(/* see above */);

onPatch(app, (patches, inversePatches) => {
 console.dir(patches, inversePatches);
});

app.todos[0].toggle();

切换todo的上述代码将以下内容打印到控制台:

// patches: 
[{
    op: "replace",
    path: "/todos/0/done",
    value: true
}]

// inverse-patches: 
[{
    op: "replace",
    path: "/todos/0/done",
    value: false
}]

中间软件

在前面的一节中,我们简要地提到了中间件,但让我们在这里进行扩展。中间件充当在状态树上调用的操作的拦截器。因为 MST 要求使用动作,所以我们确信每个动作都会通过中间件。中间件的存在使得实现几个横切功能变得非常简单,例如:

  • 登录中
  • 认证
  • 时间旅行
  • 取消/重做

事实上,mst-middlewaresNPM 包包含前面提到的一些中间软件,以及其他一些。有关这些中间件的更多详细信息,请参阅:https://github.com/mobxjs/mobx-state-tree/blob/master/packages/mst-middlewares/README.md

进一步阅读

我们几乎没有触及 MobX 状态树的表面,但希望它在组织和构造 MobX 中的可观察状态方面留下了印象。这是一种定义明确、由社区驱动的方法,包含了本书中讨论的许多最佳实践。对于 MST 的更深入的探索,您可以参考官方的入门指南:https://github.com/mobxjs/mobx-state-tree/blob/master/docs/getting-started.md#getting-开始

总结

在本章中,我们介绍了采用带有包(如mobx-utilsmobx-state-tree)的 MobX 的实际方面。这些软件包编纂了关于在各种场景中使用 MobX 的社区智慧。

mobx-utils为您提供了一组实用程序,用于处理异步任务、处理昂贵的更新、创建用于事务性编辑的视图模型等等。

mobx-state-tree是一个全面的软件包,旨在通过 MobX 简化应用开发。它采用规定性的方法来构造和组织 MobX 中的可观察状态。通过这种声明性方法,MST 能够更深入地理解状态树,并提供多种功能,如运行时类型检查、快照、JSON 修补程序、中间件等。总的来说,它有助于开发 MobX 应用的清晰心智模型,并将类型化域模型放在首位。

在下一章中,我们将通过对 MobX 内部工作原理的窥视,来结束 MobX 的旅程。如果 MobX 的某些部分看起来像黑魔法,那么下一章将消除所有此类神话。