mobxapi 界面非常精简,并为处理状态管理逻辑提供了正确的抽象。在大多数情况下,我们到目前为止看到的 API 就足够了。然而,总会有一些粗糙的边缘情况需要稍微偏离踏实的道路。MobX 为这些通道提供了一些特殊的 API。我们将在本章中介绍其中一些。
本章将介绍的主题包括:
- 使用对象 API 进行直接操作
- 使用
inject()和observe()钩住内部 MobX 事件系统 - 有助于调试的特殊实用程序功能和工具
- 快速提及一些杂项 API
最后,要使用本书的 Git 存储库,用户需要安装 Git。
本章代码文件可在 GitHub 上找到: https://github.com/PacktPublishing/Mobx-Quick-Start-Guide/tree/master/src/Chapter07
查看以下视频以查看代码的运行: http://bit.ly/2A1Or6V
在决定可观察状态的数据结构时,您的自然选择应该是接触observable.object()、observable.array()、observable.map()、observable.box(),或者使用方便的observable()API。操作这些数据结构就像直接改变属性或根据需要添加和删除元素一样简单。
MobX 为您提供了另一种通过外科手术更改数据结构的方法。它公开了一个粒度对象 API,可以在运行时改变这些数据结构。事实上,它为您提供了一些甚至在原始数据结构中都不可能实现的功能。例如,向可观察对象添加新属性,同时保持其反应性
对象 API 的重点是为您提供对顶级数据结构(对象、数组和映射)的可观察属性的粒度控制。在这样做的过程中,他们继续与 MobX 反应系统配合良好,并确保您所做的颗粒变化被反应所拾取。以下 API 适用于可观测对象/阵列/地图:
get(thing, key):检索键下的值。这把钥匙甚至可能不存在。在反应中使用时,当该键可用时,它将触发重新执行。set(thing, key, value)或set(thing, { key: value }):设置键的值。第二种形式更适合一次设置多个键值对。在概念上,它与Object.assign()非常相似,但增加了反应性。has(thing, key):返回一个布尔值,指示该键是否存在。remove(thing, key):删除给定的键及其值。values(thing):给出一个值数组。keys(thing):给出一个包含所有键的数组。请注意,这仅适用于可观察对象和贴图。entries(thing):返回一个键值对数组,其中每一对都是两个元素的数组([key, value])。
以下代码段练习了所有这些 API:
import {
autorun,
observable,
set,
get,
has,
toJS,
runInAction,
remove,
values,
entries,
keys,
} from 'mobx';
class Todo {
@observable description = '';
@observable done = false;
constructor(description) {
this.description = description;
}
}
const firstTodo = new Todo('Write Chapter');
const todos = observable.array([firstTodo]);
const todosMap = observable.map({
'Write Chapter': firstTodo,
});
// Reactions to track changes
autorun(() => {
console.log(`metadata present: ${has(firstTodo, 'metadata')}`);
console.log(get(firstTodo, 'metadata'), get(firstTodo, 'user'));
console.log(keys(firstTodo));
});
autorun(() => {
// Arrays
const secondTodo = get(todos, 1);
console.log('Second Todo:', toJS(secondTodo));
console.log(values(todos), entries(todos));
});
// Granular changes
runInAction(() => {
set(firstTodo, 'metadata', 'new Metadata');
set(firstTodo, { metadata: 'meta update', user: 'Pavan Podila' });
set(todos, 1, new Todo('Get it reviewed'));
});
runInAction(() => {
remove(firstTodo, 'metadata');
remove(todos, 1);
});通过使用这些 API,您可以针对可观察对象的特定属性,并根据需要进行更新。对于对象 API,读取和写入不存在的键被认为是有效的。请注意我们是如何在autorun()中读取firstTodo的metadata属性的,在调用时它并不存在。但是,由于使用了get()API,MobX 仍然跟踪该密钥。当我们set()之后在一个动作中metadata时,autorun()被重新触发,在控制台上打印出来
这可以在以下控制台输出中看到。注意metadata检查在移除时如何从false到true再回到false:
metadata present: false undefined undefined
(2) ["description", "done"]
Second Todo: undefined
[Todo] [Array(2)]
metadata present: true meta update Pavan Podila
(4) ["description", "done", "metadata", "user"]
Second Todo: {description: "Get it reviewed", done: false}
(2) [Todo, Todo] (2) [Array(2), Array(2)]
metadata present: false undefined "Pavan Podila"
(3) ["description", "done", "user"]
Second Todo: undefined
[Todo] [Array(2)]所有的可观察类型都是由 MobX 创建的特殊类,它们不仅存储数据,而且还存储一系列用于跟踪更改的内务处理。我们将在后面的一章中探讨这一内务管理,但对于我们现在的讨论,可以说这些 MobX 类型并不总是与其他第三方 api 兼容,特别是在使用 mobx4 时。
当与外部库接口时,您可能需要发送原始 JavaScript 值,而不是 MobX 类型的值。这就是你需要toJS()功能的地方。它将 MobX 观察值转换为原始 JavaScript 值:
toJS(source, options?)
source:任何可观察的盒子、对象、数组、地图或原语。
options:控制行为的可选参数,如:
exportMapsAsObject(布尔值):是将可观察映射序列化为对象(当true时)还是将其序列化为 JavaScript 映射(当false时)。默认为true。detectCycles(布尔值):默认设置为true。它在序列化期间检测循环引用,并重用已序列化的对象。在大多数情况下,这是一个很好的默认值,但出于性能原因,当您确定没有循环引用时,可以将其设置为false。
toJS()需要注意的一个重要点是,它不会序列化计算属性。这是有意义的,因为它是纯派生的信息,可以随时重新计算。toJS()的目的只是序列化核心可观察状态。类似地,可观测数据的任何不可枚举属性都不会被序列化,也不会递归到任何不可观测数据结构中。
在下面的示例中,您可以看到toJS()API 是如何应用于可观察对象的:
const number = observable.box(10);
const cart = observable({
items: [{ title: 'milk', quantity: 2 }, { title: 'eggs', quantity: 3 }],
});
console.log(toJS(number));
console.log('MobX type:', cart);
console.log('JS type:', toJS(cart));控制台输出显示应用toJS()API 前后可观察到的cart:
10
MobX type: Proxy {Symbol(mobx administration): ObservableObjectAdministration$$1}
JS type: {items: Array(2)}我们在前面章节中看到的 API 允许您创建可观察的对象,并通过反应对更改做出反应。MobX 还为您提供了一种方法,可以利用内部流动的事件使反应式系统工作。通过将侦听器附加到这些事件,您可以微调一些昂贵资源的使用,或者控制允许哪些更新应用于可观察对象。
通常,反应是我们阅读观察到的并应用一些副作用的地方。这告诉 MobX 开始跟踪可观察到的并重新触发对更改的反应。然而,如果我们从被观察物的角度来看,它如何知道何时被反应使用?当它在反应中被读取时,如何进行一次性设置,当它不再被使用时如何进行清理?
我们在这里需要的是能够知道何时可观测到可观测到和何时可观测到不可观测到:在 MobX 反应系统中,它变为活跃和不活跃的两个时间点。为此,我们有以下恰当命名的 API:
disposer = onBecomeObserved(observable, property?: string, listener: () => void)disposer = onBecomeUnobserved(observable, property?: string, listener: () => void)
observable:可以是盒装的可观察对象、可观察对象/阵列/地图。
property:可观察对象的可选属性。指定属性与直接引用属性有根本不同。例如,onBecomeObserved(cart, 'totalPrice', () => {})与onBecomeObserved(cart.totalPrice, () => {})不同。在第一种情况下,MobX 将能够跟踪可观察属性,但在第二种情况下,它无法跟踪,因为它只接收值而不是属性。事实上,MobX 会抛出一个Error,表示cart.totalPrice的情况下没有可跟踪的内容:
Error: [mobx] Cannot obtain atom from 0前面的错误现在可能没有多大意义,尤其是原子这个术语。我们将在第 9 章、Mobx 内部中详细介绍原子。
disposer:这些处理程序的返回值。这是一个可以用来处理这些处理程序和清理事件连接的函数。
下面的代码片段显示了这些 API 的作用:
import {
onBecomeObserved,
onBecomeUnobserved,
observable,
autorun,
} from 'mobx';
const obj = observable.box(10);
const cart = observable({
items: [],
totalPrice: 0,
});
onBecomeObserved(obj, () => {
console.log('Started observing obj');
});
onBecomeUnobserved(obj, () => {
console.log('Stopped observing obj');
});
onBecomeObserved(cart, 'totalPrice', () => {
console.log('Started observing cart.totalPrice');
});
onBecomeUnobserved(cart, 'totalPrice', () => {
console.log('Stopped observing cart.totalPrice');
});
const disposer = autorun(() => {
console.log(obj.get(), `Cart total: ${cart.totalPrice}`);
});
setTimeout(disposer);
obj.set(20);
cart.totalPrice = 100;在前面的代码段中,当autorun()第一次执行时,将调用onBecomeObserved()处理程序。调用disposer函数时,将调用onBecomeUnobserved()处理程序。这可以在以下控制台输出中看到:
Started observing obj
Started observing cart.totalPrice
10 "Cart total: 0"
20 "Cart total: 0"
20 "Cart total: 100"
Stopped observing cart.totalPrice
Stopped observing objonBecomeObserved() and onBecomeUnobserved() are great hooks to lazily set up (and tear down) an observable on its first use (and last use). This is useful in cases where there might be an expensive operation that is required to set the initial value of the observable. Such operations can be lazily performed by deferring until it is actually used somewhere.
让我们举一个例子,我们将延迟加载一个城市的温度,但仅当它被访问时。这可以通过使用onBecomeObserved()和onBecomeUnobserved()的挂钩对可观察属性进行建模来实现。以下代码段显示了此操作:
// A mock service to simulate a network call to a weather API
const temperatureService = {
fetch(location) {
console.log('Invoked temperature-fetch');
return new Promise(resolve =>
setTimeout(resolve(Math.round(Math.random() * 35)), 200),
);
},
};
class City {
@observable temperature;
@observable location;
interval;
disposers;
constructor(location) {
this.location = location;
const disposer1 = onBecomeObserved(
this,
'temperature',
this.onActivated,
);
const disposer2 = onBecomeUnobserved(
this,
'temperature',
this.onDeactivated,
);
this.disposers = [disposer1, disposer2];
}
onActivated = () => {
this.interval = setInterval(() => this.fetchTemperature(), 5000);
console.log('Temperature activated');
};
onDeactivated = () => {
console.log('Temperature deactivated');
this.temperature = undefined;
clearInterval(this.interval);
};
fetchTemperature = flow(function*() {
this.temperature = yield temperatureService.fetch(this.location);
});
cleanup() {
this.disposers.forEach(disposer => disposer());
this.disposers = undefined;
}
}
const city = new City('Bengaluru');
const disposer = autorun(() =>
console.log(`Temperature in ${city.location} is ${city.temperature}ºC`),
);
setTimeout(disposer, 15000); 前面的控制台输出显示了temperature可观察到的激活和停用。在autorun()激活,15 秒后停用。我们启动计时器,不断更新onBecomeObserved()处理程序中的温度,并在onBecomeUnobserved()处理程序中清除它。定时器是我们管理的资源,仅在访问temperature时创建,而不是在以下时间之前创建:
Temperature activated
Temperature in Bengaluru is undefinedºC
Invoked temperature-fetch
Temperature in Bengaluru is 22ºC
Invoked temperature-fetch
Temperature in Bengaluru is 32ºC
Invoked temperature-fetch
Temperature in Bengaluru is 4ºC
Temperature deactivatedMobX 不会立即应用您对可观察对象所做的更改。相反,它们通过一层拦截器,拦截器能够保留更改、修改更改,甚至完全丢弃更改。这在intercept()API 中都是可能的。签名与onBecomeObserved和onBecomeUnobserved非常相似,回调函数(拦截器为您提供变更对象:
disposer = intercept(observable, property?, interceptor: (change) => change | null )
observable:盒装可观测或可观测对象/阵列/地图。
property:要在可观察对象上截获的属性的可选字符串名称。正如我们前面看到的onBecomeObserved和onBecomeUnobserved一样,intercept(cart, 'totalPrice', (change) => {})和intercept(cart.totalPrice, () => {})之间存在差异。对于后者(cart.totalPrice,您截取的是一个值,而不是可观察属性。MobX 将抛出一个错误,指出您没有传递正确的类型。
interceptor:接收变更对象并期望返回最终变更的回调;按原样应用、修改或放弃(null)。在拦截器中抛出错误以通知异常更新也是有效的。
disposer:返回一个函数,调用该函数将取消该拦截器。这与我们所看到的onBecomeObserved()、onBecomeUnobserved()以及autorun()、reaction()和when()等反应非常相似。
收到的 change 参数具有一些已知字段,这些字段提供了详细信息。其中最重要的是type字段,它告诉您变更的类型,以及object,它给出了发生变更的对象。根据type,其他几个字段会为更改添加更多上下文:
type:可以是添加、删除、更新object:装箱的可观察对象或可观察对象/数组/映射实例newValue:当类型为“添加”或“更新”时,此字段包含新值oldValue:当类型为删除或更新时,此字段携带上一个值
在拦截器回调中,您有机会最终确定实际要应用的更改类型。您可以执行以下操作之一:
- 返回 null 并放弃更改
- 使用其他值更新
- 抛出一个指示异常值的错误
- 按原样返回并应用更改
让我们以拦截主题更改并确保仅应用有效更新为例。在下面的代码片段中,您可以看到我们是如何截取可观察主题的color属性的。颜色可以是浅或深,也可以是l或d的速记值。对于任何其他值,我们抛出一个错误。我们还通过返回null和放弃更改来防止颜色不稳定:
import { intercept, observable } from 'mobx';
const theme = observable({
color: 'light',
shades: [],
});
const disposer = intercept(theme, 'color', change => {
console.log('Intercepting:', change);
// Cannot unset value, so discard this change
if (!change.newValue) {
return null;
}
// Handle shorthand values
const newTheme = change.newValue.toLowerCase();
if (newTheme === 'l' || newTheme === 'd') {
change.newValue = newTheme === 'l' ? 'light' : 'dark'; // set
the correct value
return change;
}
// check for a valid theme
const allowedThemes = ['light', 'dark'];
const isAllowed = allowedThemes.includes(newTheme);
if (!isAllowed) {
throw new Error(`${change.newValue} is not a valid theme`);
}
return change; // Correct value so return as-is
});与intercept()对应的实用程序是observe()。顾名思义,observe()允许您对可观察对象进行精细观察:
observe(observable, property?, observer: (change) => {})
签名与intercept()完全相同,但行为却大不相同。observe()在变更应用于可观察对象后被调用*。*
一个有趣的特征是observe()对交易免疫。这意味着,观察者回调在突变后立即被调用,并且不会等到事务完成。正如你所知,动作是突变发生的地方。MobX 通过触发通知来优化通知,但只有在最上面的操作完成之后。通过observe(),你可以在突变发生时获得未经过滤的突变视图。
It is recommended to use autorun() whenever you feel a need for observe(). Use it only when you think you need immediate notification for a mutation.
下面的示例显示了在变异可观察对象时可以观察到的各种细节。如您所见,change参数与intercept()完全相同:
import { observe, observable } from 'mobx';
const theme = observable({
color: 'light',
shades: [],
});
const disposer = observe(theme, 'color', change => {
console.log(
`Observing ${change.type}`,
change.oldValue,
'-->',
change.newValue,
'on',
change.object,
);
});
theme.color = 'dark';当您使用更多功能扩展应用时,必须了解 MobX 反应式系统的使用方式和使用时间。MobX 附带了一组调试实用程序,可以帮助您监视和跟踪其中发生的各种活动。通过这些,您可以实时查看系统内部发生的所有可观察到的变化、动作和反应。
前面我们看到了observe()函数,它允许您观察单个可观察对象发生的变化。但是,如果您想观察所有可观察对象之间发生的变化,而不必单独设置observe()处理程序,该怎么办?这就是spy()的用武之地。它可以让您深入了解系统中的各种可观察对象是如何随时间变化的:
disposer = spy(listener: (event) => { })
它接收一个侦听器函数,该函数接收一个包含所有细节的事件对象。事件的属性与observe()处理程序非常相似。有一个type字段告诉您事件的类型。该类型可以是以下类型之一:
- 更新:对象、数组、地图
- 新增:对象、数组、地图
- 删除:用于地图
- 创建:用于盒装可见光
- 动作:动作触发时
- 反应:执行
autorun()、reaction()或when()时 - 计算:用于计算属性
- 错误:在动作或反应中发现异常时
下面是一段设置spy()并将输出打印到控制台的代码。五秒钟后我们也在处理这个间谍:
import { spy } from 'mobx';
const disposer = spy(event => console.log(event));
setTimeout(disposer, 5000);// Console output
{type: "action", name: "<unnamed action>", object: undefined, arguments: Array(0), spyReportStart: true}
{type: "update", object: BookSearchStore, oldValue: 0, name: "BookSearchStore@1", newValue: 2179, …}
{spyReportEnd: true}
{object: Proxy, type: "splice", index: 0, removed: Array(0), added: Array(20), …}
{spyReportEnd: true}
{type: "update", object: BookSearchStore, oldValue: Proxy, name: "BookSearchStore@1", newValue: Proxy, …}
{spyReportEnd: true}
{type: "update", object: BookSearchStore, oldValue: "pending", name: "BookSearchStore@1", newValue: "completed", …}一些间谍事件可能伴随着spyReportStart或spyReportEnd属性。这些标志着一组相关的事件。
Using spy() directly is probably not your best option during development. It is better to rely on the visual debugger (discussed in the following section), which makes use of spy() to give you more readable logs. Note that the calls to spy() are a no-op for production builds when you set the NODE_ENV environment variable to "production".
虽然spy()为您提供了一个镜头来观察 MobX 中发生的所有变化,trace()是一个专门关注计算属性、反应和组件渲染的实用工具。您只需在计算属性、反应或组件呈现中放置一条trace()语句,即可了解调用该属性的原因:
trace(thing?, property?, enterDebugger?)
它有三个可选参数:
thing:可观察到的property:可观察的属性enterDebugger:表示是否自动进入调试器的布尔标志
使用trace(true)调用跟踪是很常见的,调用时会在调试器内暂停。对于图书搜索示例(来自第 3 章、一款带有 MobX的 React 应用),我们可以在SearchTextField组件的render()中放置一条跟踪语句:
import { trace } from 'mobx';
@inject('store')
@observer
export class SearchTextField extends React.Component {
render() {
trace(true);
/* ... */
}
}当调试器暂停时,您将获得一个完整的根本原因分析,以了解执行此计算属性、反应或渲染的原因。在 Chrome 开发工具中,您可以看到如下详细信息:
Details on the Chrome devtools
spy()和trace()对于了解 MobX 反应式系统的代码级别非常有用。但是,在开始分析如何提高性能时,可视化调试非常方便。MobX 有一个名为mobx-react-devtools的姐妹 NPM 包,它为您提供了一个简单的<DevTools />组件,可以帮助您可视化组件树对可观察对象的反应。通过在应用顶部包含此组件,您将在运行时看到工具栏:
import DevTools from 'mobx-react-devtools';
import React from 'react';
export class MobXBookApp extends React.Component {
render() {
return (
<Fragment>
<DevTools />
<RootAppComponent />
</Fragment>
);
}
}The screenshot below shows the MobX DevTools toolbar showing up in the top-right corner of the screen.
通过启用这些按钮,您可以看到哪些组件在观察值发生变化时呈现,查看连接到 DOM 元素的观察值的依赖关系树,并在执行操作/反应时打印控制台日志。组件在渲染时将以彩色矩形闪烁。矩形的颜色表示渲染所需的时间,绿色最快,红色最慢。可以观察闪烁的矩形,以确保仅重新渲染要更改的零件。这是一种很好的方法,可以识别不必要渲染的组件,并可能生成更细粒度的观察者。
The mobx-react-devtools package relies on spy() to print the console logs for executing actions and reactions.
MobX 提供的一些杂项 api 并不经常使用。为了完整起见,这里仍然值得一提
在处理 MobX 中的各种抽象(可观察到的、动作、反应)时,有时了解某个对象、函数或值是否属于某种类型是很有用的。MobX 有一套isXXXAPI,可帮助您确定值的类型:
isObservableObject(thing)、isObservableArray(thing)、isObservableMap(thing):告诉您传入的值是可观察对象、数组还是地图isObservable(thing)和isObservableProp(thing, property?):与上一点类似,但更广义地检查可观察值isBoxedObservable(thing):该值是否为装箱可见值isAction(func):如果函数被动作包装,则返回trueisComputed(thing)和isComputedProp(thing, property?):检查该值是否为计算属性
MobX 在内部构建了一个反应性结构,使所有的观察和反应保持连接。我们将在第 9 章、Mobx 内部构件中探讨这些内部构件,在这里我们将看到提到某些术语,如原子。现在,让我们快速地看一下 API,它会给你观察和反应的内部表示:
getAtom(thing, property?):每个可观察对象的核心是一个Atom,它跟踪依赖于可观察值的观察者。其目的是报告任何人读取或写入可观察值的时间。通过这个 API,您可以得到支持可观察对象的Atom实例。getDependencyTree(thing, property?):这将为您提供给定事物所依赖的依赖关系树。它可用于获取计算属性或反应的依赖项。getObserverTree(thing, property?):这是getDependencyTree()的对应项,它提供了依赖于给定事物的观察者。
尽管 MobX 有一个精简的外部级 API,但也有一组用于更细粒度的观察和变异的 API。我们看到了如何使用对象 API 在可观察树中进行非常精确的更改。通过observe()和intercept(),您可以跟踪在可观察对象中发生的更改,也可以截取以修改更改。
在调试期间,spy()和trace()是您的朋友,再加上mobx react devtools,您有一个用于识别和提高渲染性能的可视化调试器。使用 MobX 时,这些工具和实用程序为您提供了丰富的开发人员体验(DX)。
在第 8 章探索 mobx utils 和 mobx 状态树中,我们将提高使用带有特殊包mobx-utils和mobx-state-tree的 mobx 的标准。

