投身烈火
Keep Walking

MobX入坑指南(4) -- Utility functions

之前几篇大概介绍了mobx最常用的几个方法,这次准备把剩余的公共方法都介绍了。

autorunAsync

autorunAsync(debugName?: string, action: () => void, minimumDelay?: number, scope?): disposer

autorunAsync 的功能与 autorun 相似,功能都是在观测对象发生变化时自动运行回调函数 action。不同点在于 autorun 是在观测对象发生变化时立即执行的,而 autorunAsync是异步的,可以通过 minimumDelay 参数来指定延迟的时间。

如果被观测对象的在延迟过程中发生多次变化,action 也只会在延迟结束时触发一次,所以它和后面要介绍到的 transaction 方法效果类似。在某些场景下这个方法很有用,比如他可以被用来防止频繁向服务端发起请求。

如果传了 scope 参数,那么 scope 将作为 action 运行时的this。

如果传了第一个参数 debugName,那么在调试工具中将使用 debugName 作为调试信息。

autorun 一样,autorunAsync 也会返回一个销毁函数。

1
2
3
4
5
6
7
autorunAsync(() => {
// 我们假设 searchBar.keyword 已经被观测, 是搜索输入框的值。当它发生变化时我们要把它发送到服务端请求搜索结果。
// 如果这里使用autorun,那么每次变化都会向调用sendKeywordToServer。
// 使用autorunAsync延迟300ms发送,当发送时,searchBar.keyword会是这300ms内变化的最终值。
// 这样就可以有效的防止频繁请求造成服务抖动。
sendKeywordToServer(searchBar.keyword);
}, 300);

Atom类 和 Reaction类

Atom

有些时候,你可能想要有更多的数据结构或其他的东西(比如streams),也可以用于响应计算。可以使用 Atom 类简单快速的实现这一功能。Atom 实例可以通知mobx观测对象发生了变化,而mobx会在启用和停用观测对象的时候通知 Atom 实例。

下面的例子展示了 Atom 的全部功能,这个例子展示了如何创建一个时钟,这个时钟只有在被观测时才会运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import {Atom, autorun} from "mobx";
class Clock {
atom;
intervalHandler = null;
currentDateTime;
constructor() {
// 创建一个Atom实例
this.atom = new Atom(
// 第一个参数: Atom实例的名字, 调试用的
"Clock",
// 第二个参数(可选): 从不被监听到被监听时的回调函数.
() => this.startTicking(),
// 第三个参数(可选): 从被监听到不被监听时的回调函数
// 注意,atom实例会多次在这两种状态见转换
() => this.stopTicking()
);
}
getTime() {
// 如果Atom实例被响应函数调用,则reportObserved返回true。
// 同时,reportObserved会通知mobx这个实例在响应回调中被使用了,它还会触发实例的第二个参数(startTicking)
if (this.atom.reportObserved()) {
return this.currentDateTime;
} else {
// 当没有响应函数调用Atom实例的时候,就不会触发startTicking。
// 根据不同的情况,这里也可以做不同的处理,比如抛出一个错误,返回一个默认值等等。
return new Date();
}
}
tick() {
this.currentDateTime = new Date();
// 通知mobx当前值发生了变化
this.atom.reportChanged();
}
startTicking() {
this.tick(); // 初始化时钟
this.intervalHandler = setInterval(
() => this.tick(),
1000
);
}
stopTicking() {
clearInterval(this.intervalHandler);
this.intervalHandler = null;
}
}
const clock = new Clock();
const disposer = autorun(() => console.log(clock.getTime()));
// ... 每秒打印时间
disposer();
// 停止打印。如果没有响应函数调用当前clock实例,那么时钟将停止。会触发stopTicking函数。

Reaction

使用 Reaction 可以创建一个自定义的监听器。Reaction 接受一个函数作为参数,他会分析这个函数所依赖的被观测对象,然后追踪他们,当他们发生变化时发出事件。

下面的例子展示了 autorun 是如何用 Reaction 实现的,其实这个例子我没看太懂,貌似必须调用 Reaction 的track方法才能追踪并发出信号,但是例子中是在 Reaction 接收的函数中调用,然后runReaction的时候开始,具体的得等我翻了源码之后才能知道了……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function autorun(view: Lambda, scope?: any) {
if (scope)
view = view.bind(scope);
const reaction = new Reaction(view.name || "Autorun", function () {
this.track(view);
});
// 开始或者排入队列
if (isComputingDerivation() || globalState.inTransaction > 0)
globalState.pendingReactions.push(reaction);
else
reaction.runReaction();
return reaction.getDisposer();
}

createTransformer

createTransformer(transformation: (value: A) => B, onCleanup?: (result: B, value?: A) => void): (value: A) => B

createTransformer 可以将一个转换函数(可以将一个值转换为另一个值得函数,比如数组的map方法接收的函数)包装成一个可缓存的响应函数。换句话说, 如果参数transformation接收到一个值A,然后把A转化为了B,那么以后再接收到A,它就会把缓存的B返回。如果A发生了变化,那么transformation会重新计算更新B。如果没有响应函数引用这个转换函数了,那么他将自动清除自己的缓存。

使用 createTransformer 可以方便的对一个完整的数据结构进行转换(原文:it is very easy to transform a complete data graph into another data graph,我个人理解是,作者想表达这种转换方式对图这种数据结构会特别有效……)。转换函数还可以进行嵌套,这样你就可以用很多小的转换函数碎片组成一个树状结构,描述更复杂的模型。最终组成的数据模型不会过期,他会一直与组成他的转换函数碎片保持同步。这个特性能让mobx很容易实现一些强大的功能,比如sideways data loading(react的一个概念,将数据直接推送给某些具体的组件,而非从父级层层传递,数据加载后基本上无需从底层刷新app,而是刷新若干组件中某个具体的部分)、map-reduce(仿佛说的是谷歌三宝之一的MapReduce架构……map-reduce与js相关的资料我没有查到,具体MapReduce的介绍可以看这里)、追踪不可变对象变更历史,等等。

onCleanup 参数会在转换函数不再被使用时被调用,可以用来销毁资源。

转换函数需要用响应函数包装才能起作用,比如放在 @observer 或者 autorun 里。和其他的计算属性一样,如果不再有观察者调用,转换函数也将退好为惰性的,不会自动执行,以保证程序的性能。

上面说的各种概念可能会比较难理解,下面列出了两个例子来解释之前的概念:

追踪数据变化,分享数据结构

这个例子是从这里来得(我能从中看出追踪数据状态来,但是分享数据结构没看出来……):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
store用来保存boxes和arrows
*/
const store = observable({
boxes: [],
arrows: [],
selection: null
});
/**
states列表用来保存序列化之后的store历史状态
*/
const states = [];
autorun(() => {
states.push(serializeState(store));
});
const serializeState = createTransformer(store => ({
boxes: store.boxes.map(serializeBox),
arrows: store.arrows.map(serializeArrow),
selection: store.selection ? store.selection.id : null
}));
const serializeBox = createTransformer(box => ({...box}));
const serializeArrow = createTransformer(arrow => ({
id: arrow.id,
to: arrow.to.id,
from: arrow.from.id
}));

在这个例子中,states中的每个state的序列化,由三个不同的转化函数完成。autorun触发store的序列化,进而序列化所有的boxes和arrows。

让我们用一个假设的例子来看看执行的过程。假设我们往store.boxes中添加一个box,我们叫他box#3。

  1. 首先box#3会被 map 方法传入 serializeBox 函数,serializeBox 函数执行将其序列化并将结果添加进自己的缓存列表。
  2. 当另一个box别添加进store.boxes,将导致 serializeState 函数重新计算结果,从而产生一个全新的boxes列表。在这个过程中,对于已存在的值,serializeBox都将从缓存列表返回旧值,这样转换函数就不需要再次运行了。
  3. 然后,如果有人更改box#3属性,这将导致 serializeBox 重新计算box#3的值。转换函数将产生一个新的box#3的Json对象,所有订阅了这个转换函数的观察者都将再次运行。这个例子中 serializeState 会自动执行。serializeState将产生新值,映射所有的box的。除了box#3,其他所有box的值都将会从缓存列表返回。
  4. 最后,如果box#3从 store.boxes 中移除,serializeState 也将重新计算。serializeBox 不再监听box#3,监听它的响应函数也将退化为非响应模式。serializeBox 的缓存列表中也将移除box#3的缓存。

上面的例子中,我们使用不可变的状态跟踪有效的取得了状态变化列表,共享了数据结构。所有box和arrow都会被转化为简单状态树。每次计算的都会给states 中添加一条新的数据。不同的数据之间将共享box和arrow。

将一个数据结构转换为一个可响应的数据结构

这段儿我都看懵逼了……纯凭感觉理解的……下面贴上原文对比着看吧

Instead of returning plain values from a transformation function, it is also possible to return observable objects. This can be used to transform an observable data graph into a another observable data graph, which can be used to transform… you get the idea.

转换函数除了可以返回一般数据类型,还可以返回观测对象。所以也可以使用转换函数完成可观测对象间的转换。

Here is a small example that encodes a reactive file explorer that will update its representation upon each change. Data graphs that are built this way will in general react a lot faster and will consist of much more straight-forward code, compared to derived data graph that are updated using your own code. See the performance tests for some examples.

下面是个自动响应的文件管理器的例子。使用这种方式构建的数据结构,形式上更加简单直接,数据更新时响应速度也比一般的方式快的多。可以看一看这些例子的性能测试

Unlike the previous example, the transformFolder will only run once as long as a folder remains visible; the DisplayFolder objects track the associated Folder objects themselves.

不像之前的例子,如果文件夹一直可见,那么 transformFolder 只会运行一次;DisplayFolder 对象会追踪 Folder 对象的变化。

In the following example all mutations to the state graph will be processed automatically. Some examples:

下面的例子中,所有对 state 的改变都会自动处理。比如做如下操作:

  1. Changing the name of a folder will update it’s own path property and the path property of all its descendants.

    改变文件夹的名字将更新它和它的子文件夹的文件路径,

  2. Collapsing a folder will remove all descendant DisplayFolders from the tree.

    折叠一个文件夹将会移除所有子文件夹的DisplayFolder实例

  3. Expanding a folder will restore them again.

    展开文件夹时,子文件夹再都恢复回来

  4. Setting a search filter will remove all nodes that do not match the filter, unless they have a descendant that matches the filter.

    如果设置了搜索过滤条件,将会只保留符合条件的子文件夹,其他的都会移除掉。
    ……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import {extendObservable, asFlat, observable, createTransformer, autorun} from mobx;
function Folder(parent, name) {
this.parent = parent;
extendObservable(this, {
name: name,
children: asFlat([]),
});
}
function DisplayFolder(folder, state) {
this.state = state;
this.folder = folder;
extendObservable(this, {
collapsed: false,
name: function() {
return this.folder.name;
},
isVisible: function() {
return !this.state.filter || this.name.indexOf(this.state.filter) !== -1 || this.children.some(child => child.isVisible);
},
children: function() {
if (this.collapsed)
return [];
return this.folder.children.map(transformFolder).filter(function(child) {
return child.isVisible;
})
},
path: function() {
return this.folder.parent === null ? this.name : transformFolder(this.folder.parent).path + "/" + this.name;
}
});
}
var state = observable({
root: new Folder(null, "root"),
filter: null,
displayRoot: null
});
var transformFolder = createTransformer(function (folder) {
return new DisplayFolder(folder, state);
});
autorun(function() {
state.displayRoot = transformFolder(state.root);
});

expr

expr(worker: () => void)

expr 可以在计算属性的函数中创建一个临时的计算属性,其实就是computed(func).get()。作者在文档中说设计这个api的意图是为了提升计算属性的性能,比如下面的例子,如果使用 expr 替代直接用比较运算,可以利用计算属性的缓存,减少运算次数。

1
2
3
4
const TodoView = observer(({todo, editorState}) => {
const isSelected = mobx.expr(() => editorState.selection === todo);
return <div className={isSelected ? "todo todo-selected" : "todo"}>{todo.title}</div>;
});

extendObservable

extendObservable(target: object, ...properties: object)

在之前的几篇文章中,我们已经大概见过 extendObservable 应用的实例了。 extendObservableObject.assign 类似,接受多个参数,将 properties 上所有的键值对,都合并到 target 上,同时把它们都转换成可观测的属性。

如果属性值是一个没有参数的函数,那 extendObservable 将用 computed 把它转化为一个计算属性。

所以,observable(object) 其实是 extendObservable(object, object)的别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = function(firstName, lastName) {
// 在当前实例为观测对象
extendObservable(this, {
firstName: firstName,
lastName: lastName
});
}
var matthew = new Person("Zheng", "Xingcheng");
// 向观测对象上添加属性
extendObservable(matthew, {
age: 30
});

isObservable

isObservable(testValue:object, propertyName?: string)

isObservable 是用来判断一个变量是不是用observable观测对象的,如果是就会返回true,如果想看变量的某个属性是否可观测,直接传入属性的引用是不行的,需要传第二个参数 propertyName 指定要判断哪个属性,如果属性可观测,就返回true

1
2
3
4
5
6
7
8
9
10
11
var person = observable({
firstName: "Zheng",
lastName: "Xingcheng"
});
person.age = 30;
console.log(isObservable(person)); // true
console.log(isObservable(person, "firstName")); // true
console.log(isObservable(person.firstName)); // false (just a string)
console.log(isObservable(person, "age")); // false

为了细化各种类型的判断,mobx还提供了map,array,object三种类型的判断,比起 isObservable ,他们的判断标准更严格,如果类型不符合就会返回false。

isObservableMap

isObservableMap(testValue:object)

如果testValue是用 mobx.map 创建的对象,则返回true。

isObservableArray

isObservableArray(testValue:object)

如果testValue是用 mobx.observable(array) 创建的对象,则返回true。

isObservableObject

isObservableObject(testValue:object)

如果testValue是用 mobx.observable(object) 创建的对象,则返回true。

1
2
3
4
5
6
7
8
9
10
var testValue = observable({
arr: [1, 2, 3],
obj: {
x: 1
},
map: map([['y',2]])
});
console.log(isObservableMap(testValue.map)); // true
console.log(isObservableArray(testValue.arr)); // true
console.log(isObservableObject(testValue.obj)); // true

modifiers

intercept & observe

reaction

spy

spy(listener)

spy 可以注册一个全局的监听函数,监听所有的mobx发出的事件,通常是用来做log或者做调试的。

比如以下例子,会打印所有的action:

1
2
3
4
5
spy((event) => {
if (event.type === 'action') {
console.log(`${event.name} with args: ${event.arguments}`)
}
})

不同的操作,event也会不一样,下面的表格是每种事件对应的参数:

event event带的属性 是否可以嵌套发生
action name, target (scope), arguments, fn (source function of the action) yes
transaction name, target (scope) yes
scheduled-reaction object (Reaction instance) no
reaction object (Reaction instance), fn (source of the reaction) yes
compute object (ComputedValue instance), target (scope), fn (source) no
error message no
update (array) object (the array), index, newValue, oldValue yes
update (map) object (observable map instance), name, newValue, oldValue yes
update (object) object (instance), name, newValue, oldValue yes
splice (array) object (the array), index, added, removed, addedCount, removedCount yes
add (map) object, name, newValue yes
add (object) object, name, newValue yes
delete (map) object, name, oldValue yes
create (boxed observable) object (ObservableValue instance), newValue yes

toJS

toJS(value: any, supportCycles?=true: boolean)

toJS可以将一个observableObject下的转化为javascript原生的对象。他会递归转换array,object,map和基础类型的值,但是不会转换计算属性和其他不可枚举的值。默认情况下,toJS会缓存下每次运行的值,貌似作者设计这个api就是为了输出log用的,可以设置 supportCycles 参数为false来提高toJS的性能。

对于更复杂的序列化反序列化场景,mobx的作者推荐使用他开发的serializr库。

transaction

transaction(worker: () => void)

在之前的 autorunAsync 有提到过,除了 autorunAsync ,还可以使用 transaction 来做批量处理。

transaction 用来批处理一系列的更新,而不会通知观测对象,当所有更新结束,才会发出通知。transaction 接收一个没有参数的worker函数作为参数,在这个函数执行完成之前,不会通知观察者。transaction 的返回值就是worker函数的返回值。另外 transaction 是同步的,可以被嵌套,只有最外层的 transaction 执行完,才会触发响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {observable, transaction, autorun} from "mobx";
const numbers = observable([]);
autorun(() => console.log(numbers.length, "numbers!"));
// Prints: '0 numbers!'
transaction(() => {
transaction(() => {
numbers.push(1);
numbers.push(2);
});
numbers.push(3);
});
// Prints: '3 numbers!'

untracked

untracked(fn: () => void)

使用 untracked 可以创建一个不被观测的代码块,通常 untracked 需要放在 (@)action 里面才有意义,比如以下的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const person = observable({
firstName: "Michel",
lastName: "Weststrate"
});
autorun(() => {
console.log(
person.lastName,
",",
// person.firstName放在了untracked的回调里面,所以不会跟这个autorun的监听函数绑定到一起
// 在修改person.firstName时就不会触发这个监听函数
untracked(() => person.firstName)
);
});
// prints: Weststrate, Michel
person.firstName = "G.K.";
// doesn't print!
person.lastName = "Chesterton";
// prints: Chesterton, G.K.

when

when(debugName?, predicate: () => boolean, effect: () => void, scope?)

when 会感测并运行参数predicate,predicate有点类似一个计算属性,当predicate为true的时候,则自动运行effect,然后销毁自己。所以 when 是一个只运行一次的 autorun

下面这个例子展示了用 when 来实现自动销毁组件的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyResource {
constructor() {
when(
// 当断言为真...
() => !this.isVisible,
// ... 则运行一次然后销毁
() => this.dispose()
);
}
@computed get isVisible() {
// 返回组件是否可见
}
dispose() {
// 销毁组件
}
}

总结

本篇整理了下mobx的公共方法和作用。就此,基础api算是都介绍完了。之后会再着重写些使用方法和介绍mobx原理的内容。

话说最近懒癌又开始发作了……_(:3 」∠)_……看着朋友们跟打了鸡血一样写那么多blog好着急的说……希望以后能迎头赶上吧……