MobX响应的常见误区
一般情况下,MobX 会按照我们的预期进行工作,但在一些场景下,如果不真正理解 MobX 到底是对什么进行响应,就会写出错误的代码。这里总结了 3 个常见的误区:
(1)MobX 是通过追踪属性的访问来追踪值的变化,而不是直接追踪值本身的变化。所以,必须在 MobX 的 derivation(computed value 和 reaction)中解引用(dereference)可观测对象的属性,才能正确观测到这些属性值的变化。例如:
var todo = observable({
title: "Learn React"
})
autorun(() => {
console.log(todo.title)
})
todo = observable({title: "Bar"})
javascript
解引用(dereference)指根据引用获取引用指向的值的过程。它和引用(reference)过程是两个相反的过程。引用过程可以记作:reference of B ⇒ A,解引用过程可以记作:dereference of A ⇒ B。 |
autorun 不会有响应。todo 虽然被改变了,但它只是一个指向一个可观测对象的变量(引用),它本身并不是可观测的。正确的写法是:
var todo = observable({
title: "Learn React"
})
autorun(() => {
console.log(todo.title)
})
todo.title = "Bar"
javascript
在 autorun 这个 reaction 中解引用 title 属性的值(通过 todo.title 的访问方式解引用),所以 title 的变化可以被正确追踪。
再考虑下面的例子:
var todo = observable({
title: "Learn React"
})
var title = todo.title;
autorun(() => {
console.log(title)
})
title = "Bar"
javascript
autorun 不会有响应。todo.title 在 autorun 外部被解引用,autorun 内部使用的 title 变量只是一个字符串类型的值,是不可观测的。MobX 必须通过追踪 todo.title 来追踪 title 属性值的变化。
最后一个例子:
var todo = observable({
task: {
title: "Learn React",
content: "Read more books about React"
}
})
var task = todo.task;
autorun(() => {
console.log(task.title)
})
todo.task.title = "Bar"; // 修改1
todo.task = {
title: "Learn MobX",
content: "Read more books about MobX"
};
javascript
这个例子更具有迷惑性。修改 1 会触发 autorun 响应,修改 2 不会触发 autorun 响应。原因是 todo.task 和 task 变量是指向同一个可观测对象的引用,在 autorun 内部解引用 task.title,修改 1 对 title 属性的修改当然可以被 autorun 追踪到;修改 2 改变的是 todo 的 task 属性,在 autorun 内部并没有解引用 task 属性,所以 task 属性值的变化无法被 autorun 追踪到。
(2)MobX 只追踪同步执行过程中的数据。
例如:
var todo = observable({
title: "Learn React"
});
autorun(() => {
setTimeout(() => console.log(todo.title), 100);
});
todo.title = "Bar";
javascript
autorun 不会有响应。autorun 执行期间并没有访问任何可观测对象,todo 是在 setTimeout 异步执行期间访问的。
(3)observer 创建的组件,只有当前组件 render 方法中直接使用的数据才会被追踪,例如:
const MyComponent = observer(({ todo }) =>
<SomeContainer
title = {() => <div>{todo.title}</div>}
/>
)
// 注意,这只是个示例,修改 todo.title 的正确方式是在 MyComponent 的父组件中完成
todo.title = "Bar" // 组件不会重新渲染
javascript
这个例子中,SomeContainer 组件的 title 是一个回调函数,用于渲染 title。虽然看似 todo.title 是在 MyComponent 的 render 方法中使用的,但并不是直接使用的,因为回调函数 title 的执行是在 SomeContainer 内,回调函数 title 执行时,todo.title 才是直接使用的。要想让 SomeContainer 可以正确响应 todo.title 的变化,SomeContainer 本身也需要使用 observer 包装。
如果 SomeContainer 来自外部库,就不方便直接使用 observer 包装 SomeContainer。这时候,SomeContainer 的 title 回调函数中可以使用一个可观测的组件,响应 todo.title 的变化:
const MyComponent = observer(({ todo }) =>
<SomeContainer
title = { () => <TitleRenderer todo={todo} />}
/>
)
// TitleRenderer 是一个可观测组件,SomeContainer 通过使用 TitleRender,响应 title 的变化
const TitleRenderer = observer(({ todo }) =>
<div>{todo.title}</div>
)
todo.title = "Bar" // 组件会重新渲染
javascript
还有另一种方案是使用 mobx-react 包提供的 Observer 组件,它不接收参数,只需要单个 render 函数作为子节点:
import { Observer } from "mobx-react";
const MyComponent = ({ todo }) =>
<SomeContainer
title = {() =>
<Observer>
{ () => <div>{todo.title}</div> }
</Observer>
}
/>
todo.title = "Bar" // 组件会重新渲染
javascript