Mobx4 对 Observables 响应

(@)computed

计算值(computed values)是可以根据现有的状态或其它计算值衍生出的值。computedautorun 都是响应式调用的表达式。

如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed;如果你不想产生一个新值,而想要达到一个效果,请使用 autorun。

如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。

@computed

如果已经启用 decorators 的话,可以在任意类属性的 getter 上使用 @computed 装饰器来声明式的创建计算属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { observable, computed } from "mobx";

class OrderLine {
@observable price = 0;
@observable amount = 1;

constructor(price) {
this.price = price;
}

@computed get total() {
return this.price * this.amount;
}
}

计算值的 setter

可以为计算值定义 setter。这些 setters 不能用来直接改变计算属性的值,但是它们可以用来作“逆向”衍生。

1
2
3
4
5
6
7
8
9
10
const orderLine = observable.object({
price: 0,
amount: 1,
get total() {
return this.price * this.amount;
},
set total(total) {
this.price = total / this.amount; // 从 total 中推导出 price
},
});

computed(expression) 函数

computed 还可以直接当做函数来调用。 就像 observable.box(primitive value) 创建一个独立的 observable。 在返回的对象上使用 .get() 来获取计算的当前值,或者使用 .observe(callback) 来观察值的改变。

computed 的选项
当使用 computed 作为调节器或者盒子,它接收的第二个选项参数对象,选项参数对象有如下可选参数:

  • name: 字符串, 在 spy 和 MobX 开发者工具中使用的调试名称
  • context: 在提供的表达式中使用的 this
  • set: 要使用的 setter 函数。 没有 setter 的话无法为计算值分配新值。 如果传递给 computed 的第二个参数是一个函数,那么就把会这个函数作为 setter
  • equals: 默认值是 comparer.default 。它充当比较前一个值和后一个值的比较函数。如果这个函数认为前一个值和后一个值是相等的,那么观察者就不会重新评估。这在使用结构数据和来自其他库的类型时很有用。例如,一个 computed 的 moment 实例可以使用 (a, b) => a.isSame(b) 。如果想要使用结构比较来确定新的值是否与上个值不同 (并作为结果通知观察者),comparer.deep 十分便利。
  • requiresReaction: 对于非常昂贵的计算值,推荐设置成 true 。如果你尝试读取它的值,但某些观察者没有跟踪该值(在这种情况下,MobX 不会缓存该值),则会导致计算结果丢失,而不是进行昂贵的重新评估。
  • keepAlive: 如果没有任何人观察到,则不要使用此计算值。 请注意,这很容易导致内存泄漏,因为它会导致此计算值使用的每个 observable ,并将计算值保存在内存中!

错误处理

如果计算值在其计算期间抛出异常,则此异常将捕获并在读取其值时重新抛出。

1
2
3
4
5
6
const x = observable.box(3);
const y = observable.box(1);
const divided = computed(() => {
if (y.get() === 0) throw new Error("Division by zero");
return x.get() / y.get();
});

Autorun

当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun。相比之下,computed(function) 创建的函数只有当它有自己的观察者时才会重新计算,否则它的值会被认为是不相关的。

如果你有一个函数应该自动运行,但不会产生一个新的值,请使用 autorun。其余情况都应该使用 computed。

Autoruns 是关于 启动效果 (initiating effects) 的 ,而不是产生新的值。
如果字符串作为第一个参数传递给 autorun ,它将被用作调试名。

Autorun 接收第二个参数,它是一个参数对象,有如下可选的参数:

  • delay: 可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。
  • name: 字符串,用于在例如像 spy 这样事件中用作此 reaction 的名称。
  • onError: 用来处理 reaction 的错误,而不是传播它们。
  • scheduler: 设置自定义调度器以决定如何调度 autorun 函数的重新运行。

delay 选项

1
2
3
4
5
6
7
8
9
autorun(
() => {
// 假设 profile.asJson 返回的是 observable Json 表示,
// 每次变化时将其发送给服务器,但发送前至少要等300毫秒。
// 当发送后,profile.asJson 的最新值会被使用。
sendProfileToServer(profile.asJson);
},
{ delay: 300 }
);

onError 选项

在 autorun 和所有其他类型 reaction 中抛出的异常会被捕获并打印到控制台,但不会将异常传播回原始导致异常的代码。 这是为了确保一个异常中的 reaction 不会阻止其他可能不相关的 reaction 的预定执行。 这也允许 reaction 从异常中恢复; 抛出异常不会破坏 MobX 的跟踪,因此如果除去异常的原因,reaction 的后续运行可能会再次正常完成。

可以通过提供 onError 选项来覆盖 Reactions 的默认日志行为。 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
const age = observable.box(10);

const dispose = autorun(
() => {
if (age.get() < 0) throw new Error("Age should not be negative");
console.log("Age", age.get());
},
{
onError(e) {
window.alert("Please enter a valid age");
},
}
);

when

when 观察并运行给定的 predicate,直到返回 true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。

对于以响应式方式来进行处理或者取消,此函数非常有用。 示例:

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() {
// 清理
}
}

Reaction

1
reaction(() => data, (data, reaction) => { sideEffect }, options?)

autorun 的变种,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果函数)的输入。

Reaction 接收第三个参数,它是一个参数对象,有如下可选的参数:

  • fireImmediately: 布尔值,用来标识效果函数是否在数据函数第一次运行后立即触发。默认值是 false 。
  • delay: 可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。
  • equals: 默认值是 comparer.default 。如果指定的话,这个比较器函数被用来比较由 数据 函数产生的前一个值和后一个值。只有比较器函数返回 false 效果 函数才会被调用。此选项如果指定的话,会覆盖 compareStructural 选项。
  • name: 字符串,用于在例如像 spy 这样事件中用作此 reaction 的名称。
  • onError: 用来处理 reaction 的错误,而不是传播它们。
  • scheduler: 设置自定义调度器以决定如何调度 autorun 函数的重新运行

在下面的示例中,reaction1、reaction2 和 autorun1 都会对 todos 数组中的 todo 的添加、删除或替换作出反应。 但只有 reaction2 和 autorun 会对某个 todo 的 title 变化作出反应,因为在 reaction2 的数据表达式中使用了 title,而 reaction1 的数据表达式没有使用。

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
const todos = observable([
{
title: "Make coffee",
done: true,
},
{
title: "Find biscuit",
done: false,
},
]);

// reaction 的错误用法: 对 length 的变化作出反应, 而不是 title 的变化!
const reaction1 = reaction(
() => todos.length,
(length) =>
console.log("reaction 1:", todos.map((todo) => todo.title).join(", "))
);

// reaction 的正确用法: 对 length 和 title 的变化作出反应
const reaction2 = reaction(
() => todos.map((todo) => todo.title),
(titles) => console.log("reaction 2:", titles.join(", "))
);

// autorun 对它函数中使用的任何东西作出反应
const autorun1 = autorun(() =>
console.log("autorun 1:", todos.map((todo) => todo.title).join(", "))
);

todos.push({ title: "explain reactions", done: false });
// 输出:
// reaction 1: Make coffee, find biscuit, explain reactions
// reaction 2: Make coffee, find biscuit, explain reactions
// autorun 1: Make coffee, find biscuit, explain reactions

todos[0].title = "Make tea";
// 输出:
// reaction 2: Make tea, find biscuit, explain reactions
// autorun 1: Make tea, find biscuit, explain reactions

@observer

observer 函数/装饰器可以用来将 React 组件转变成响应式组件。 它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。

observer 是由单独的 mobx-react 包提供的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { observer } from "mobx-react";

var timerData = observable({
secondsPassed: 0,
});

setInterval(() => {
timerData.secondsPassed++;
}, 1000);

@observer
class Timer extends React.Component {
render() {
return <span>Seconds passed: {this.props.timerData.secondsPassed} </span>;
}
}

ReactDOM.render(<Timer timerData={timerData} />, document.body);

可观察的局部组件状态

就像普通类一样,你可以通过使用 @observable 装饰器在 React 组件上引入可观察属性。 这意味着你可以在组件中拥有功能同样强大的本地状态(local state),而不需要通过 React 的冗长和强制性的 setState 机制来管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { observer } from "mobx-react";
import { observable } from "mobx";

@observer
class Timer extends React.Component {
@observable secondsPassed = 0;

componentWillMount() {
setInterval(() => {
this.secondsPassed++;
}, 1000);
}

render() {
return <span>Seconds passed: {this.secondsPassed} </span>;
}
}

ReactDOM.render(<Timer />, document.body);

使用 inject 将组件连接到提供的 stores

mobx-react 包还提供了 Provider 组件,它使用了 React 的上下文(context)机制,可以用来向下传递 stores。 要连接到这些 stores,需要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const colors = observable({
foreground: '#000',
background: '#fff'
});

const App = () =>
<Provider colors={colors}>
<app stuff... />
</Provider>;

const Button = inject("colors")(observer(({ colors, label, onClick }) =>
<button style={{
color: colors.foreground,
backgroundColor: colors.background
}}
onClick={onClick}
>{label}</button>
));

// 稍后..
colors.foreground = 'blue';
// 所有button都会更新

componentWillReact (生命周期钩子)

由 mobx-react 提供。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact 会被触发。

1
2
3
4
5
6
7
8
9
10
11
12
import { observer } from "mobx-react";

@observer
class TodoView extends React.Component {
componentWillReact() {
console.log("I will re-render, since the todo has changed!");
}

render() {
return <div>this.props.todo.title</div>;
}
}

componentWillReact 初始化渲染前不会触发 (使用 componentWillMount 替代)

componentWillReact 对于 mobx-react@4+, 当接收新的 props 时并在 setState 调用后会触发此钩子

MobX 会对什么作出反应?

MobX 会对在追踪函数执行过程中读取现存的可观察属性做出反应。

  • “读取” 是对象属性的间接引用,可以用过 . (例如 user.name) 或者 [] (例如 user[‘name’]) 的形式完成。
  • “追踪函数” 是 computed 表达式、observer 组件的 render() 方法和 when、reaction 和 autorun 的第一个入参函数。
  • “过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。

换句话说,MobX 不会对其作出反应:

  • 从 observable 获取的值,但是在追踪函数之外
  • 在异步调用的代码块中读取的 observable