Computed的使用
computed计算属性,依赖缓存
如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。 计算值在大多数情况下可以被 MobX 优化的,因为它们被认为是纯函数。 例如,如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。 如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。 在这种情况下,它将被暂停。
这个自动地暂停是非常方便的。如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。而 autorun
中的值必须要手动清理才行,这点和计算值是有所不同的。 如果你创建一个计算属性,但不在 reaction 中的任何地方使用它,它不会缓存值并且有些重新计算看起来似乎是没有必要的。这点有时候会让刚接触 MobX 的人们很困惑。 然而,在现实开发场景中,这是迄今为止最好的默认逻辑。如果你需要的话,可以使用 observe
或 keepAlive
来强制保持计算值总是处于唤醒状态。
注意计算属性是不可枚举的,它们也不能在继承链中被覆盖。
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;
}
}
import {decorate, observable, computed} from "mobx";
class OrderLine {
price = 0;
amount = 1;
constructor(price) {
this.price = price;
}
get total() {
return this.price * this.amount;
}
}
decorate(OrderLine, {
price: observable,
amount: observable,
total: computed
})
// observable.object 和 extendObservable
// 都会自动将 getter 属性推导成计算属性,所以下面这样就足够了
const orderLine = observable.object({
price: 0,
amount: 1,
get total() {
return this.price * this.amount
}
})
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
}
})
class Foo {
@observable length = 2;
@computed get squared() {
return this.length * this.length;
}
set squared(value) { // 这是一个自动的动作,不需要注解
this.length = Math.sqrt(value);
}
}
computed
的选项
当使用 computed
作为调节器或者盒子,它接收的第二个选项参数对象,选项参数对象有如下可选参数:
name
: 字符串, 在 spy 和 MobX 开发者工具中使用的调试名称context
: 在提供的表达式中使用的this
set
: 要使用的setter函数。 没有 setter 的话无法为计算值分配新值。 如果传递给computed
的第二个参数是一个函数,那么就把会这个函数作为 setterequals
: 默认值是comparer.default
。它充当比较前一个值和后一个值的比较函数。如果这个函数认为前一个值和后一个值是相等的,那么观察者就不会重新评估。这在使用结构数据和来自其他库的类型时很有用。例如,一个 computed 的 moment 实例可以使用(a, b) => a.isSame(b)
。如果想要使用结构比较来确定新的值是否与上个值不同 (并作为结果通知观察者),comparer.deep
十分便利。requiresReaction
: 对于非常昂贵的计算值,推荐设置成true
。如果你尝试读取它的值,但某些观察者没有跟踪该值(在这种情况下,MobX 不会缓存该值),则会导致计算结果丢失,而不是进行昂贵的重新评估。keepAlive
: 如果没有任何人观察到,则不要使用此计算值。 请注意,这很容易导致内存泄漏,因为它会导致此计算值使用的每个 observable ,并将计算值保存在内存中!
@computed.struct
用于比较结构
@computed
装饰器不需要接收参数。如果你想创建一个能进行结构比较的计算属性时,请使用 @computed.struct
。
内置比较器
MobX 提供了三个内置 comparer
(比较器) ,它们应该能满足绝大部分需求:
comparer.identity
: 使用恒等 (===
) 运算符来判定两个值是否相同。comparer.default
: 等同于comparer.identity
,但还认为NaN
等于NaN
。comparer.structural
: 执行深层结构比较以确定两个值是否相同。computed错误处理
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()
})
divided.get() // 返回 3
y.set(0) // OK
divided.get() // 报错: Division by zero
divided.get() // 报错: Division by zero
y.set(2)
divided.get() // 已恢复; 返回 1.5
Autorun
当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun
。 这通常是当你需要从反应式代码桥接到命令式代码的情况,例如打印日志、持久化或者更新UI的代码。 当使用 autorun
时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。
经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun
。 其余情况都应该使用 computed
。 Autoruns 是关于 启动效果 (initiating effects) 的 ,而不是产生新的值。 如果字符串作为第一个参数传递给 autorun
,它将被用作调试名。
传递给 autorun 的函数在调用后将接收一个参数,即当前 reaction(autorun),可用于在执行期间清理 autorun。
就像 @ observer
装饰器/函数,autorun
只会观察在执行提供的函数时所使用的数据。
var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a, b) => a + b, 0));
var disposer = autorun(() => console.log(sum.get()));
// 输出 '6'
numbers.push(4);
// 输出 '10'
disposer();
numbers.push(5);
// 不会再输出任何值。`sum` 不会再重新计算。
选项
Autorun 接收第二个参数,它是一个参数对象,有如下可选的参数:
delay
: 可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。name
: 字符串,用于在例如像spy
这样事件中用作此 reaction 的名称。onError
: 用来处理 reaction 的错误,而不是传播它们。scheduler
: 设置自定义调度器以决定如何调度 autorun 函数的重新运行delay
选项autorun(() => {
// 假设 profile.asJson 返回的是 observable Json 表示,
// 每次变化时将其发送给服务器,但发送前至少要等300毫秒。
// 当发送后,profile.asJson 的最新值会被使用。
sendProfileToServer(profile.asJson);
}, { delay: 300 });
Copy
在 autorun 和所有其他类型 reaction 中抛出的异常会被捕获并打印到控制台,但不会将异常传播回原始导致异常的代码。 这是为了确保一个异常中的 reaction 不会阻止其他可能不相关的 reaction 的预定执行。 这也允许 reaction 从异常中恢复; 抛出异常不会破坏 MobX的跟踪,因此如果除去异常的原因,reaction 的后续运行可能会再次正常完成。onError
选项
可以通过提供onError
选项来覆盖 Reactions 的默认日志行为。 示例:
一个全局的 onError 处理方法可以使用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")
}
})
Copy
onReactionError(handler)
来设置。这在测试或监控中很有用。When
when(predicate: () => boolean, effect?: () => void, options?)
when
观察并运行给定的predicate
,直到返回true。 一旦返回 true,给定的effect
就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。
对于以响应式方式来进行处理或者取消,此函数非常有用。 示例:class MyResource {
constructor() {
when(
// 一旦...
() => !this.isVisible,
// ... 然后
() => this.dispose()
);
}
@computed get isVisible() {
// 标识此项是否可见
}
dispose() {
// 清理
}
}
Copy
when-promise
如果没提供effect
函数,when
会返回一个Promise
。它与async / await
可以完美结合。async function() {
await when(() => that.isVisible)
// 等等..
}
Reaction
用法: reaction(() => data, (data, reaction) => { sideEffect }, options?)
。autorun
的变种,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个(数据函数)是用来追踪并返回数据作为第二个函数(效果 函数)的输入。 不同于 autorun
的是当创建时效果 函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。 在执行 效果 函数时访问的任何 observable 都不会被追踪。reaction
返回一个清理函数。
传入 reaction
的第二个函数(副作用函数)当调用时会接收两个参数。 第一个参数是由 data 函数返回的值。 第二个参数是当前的 reaction,可以用来在执行期间清理 reaction
值得注意的是 效果 函数仅对数据函数中访问的数据作出反应,这可能会比实际在效果函数使用的数据要少。 此外,效果 函数只会在表达式返回的数据发生更改时触发。 换句话说: reaction
需要你生产 效果 函数中所需要的东西。
选项
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
的数据表达式没有使用。autorun
追踪完整的副作用,因此它将始终正确触发,但也更容易意外地访问相关数据。 还可参见 MobX 会对什么作出反应?.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
Copy
在下面的示例中,
reaction3
会对counter
中的 count 作出反应。 当调用reaction
时,第二个参数会作为清理函数使用。 下面的示例展示了reaction
只会调用一次。const counter = observable({ count: 0 });
// 只调用一次并清理掉 reaction : 对 observable 值作出反应。
const reaction3 = reaction(
() => counter.count,
(count, reaction) => {
console.log("reaction 3: invoked. counter.count = " + count);
reaction.dispose();
}
);
counter.count = 1;
// 输出:
// reaction 3: invoked. counter.count = 1
counter.count = 2;
// 输出:
// (There are no logging, because of reaction disposed. But, counter continue reaction)
console.log(counter.count);
// 输出:
// 2
Copy
粗略地讲,reaction 是
computed(expression).observe(action(sideEffect))
或autorun(() => action(sideEffect)(expression))
的语法糖。@observer
observer
函数/装饰器可以用来将 React 组件转变成响应式组件。 它用mobx.autorun
包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。observer
是由单独的mobx-react
包提供的。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);
小贴士: 当
observer
需要组合其它装饰器或高阶组件时,请确保observer
是最深处(第一个应用)的装饰器,否则它可能什么都不做。
注意,使用@observer
装饰器是可选的,它和observer(class Timer ... { })
达到的效果是一样的。陷阱: 组件中的间接引用值
MobX 可以做很多事,但是它无法使原始数据类型值转变成可观察的(尽管它可以用对象来包装它们,参见 boxed observables)。 所以值是不可观察的,但是对象的属性可以。这意味着
@observer
实际上是对间接引用(dereference)值的反应。 那么在上面的示例中,如果是用下面这种方式初始化的,Timer
组件是不会有反应的:React.render(<Timer timerData={timerData.secondsPassed} />, document.body)
在这个代码片段中只是把
secondsPassed
的当前值传递给了Timer
组件,这个值是不可变值0
(JS中所有的原始类型值都是不可变的)。 这个数值永远都不会改变,因此Timer
组件不会更新。secondsPassed
的值将来会发生改变, 所以我们需要在组件中访问它。或者换句话说: 值需要通过引用来传递而不是通过(字面量)值来传递。ES5 支持
在ES5环境中,可以简单地使用
observer(React.createClass({ ...
来定义观察者组件。还可以参见语法指南。无状态函数组件
上面的
Timer
组件还可以通过使用observer
传递的无状态函数组件来编写:import {observer} from "mobx-react";
const Timer = observer(({ timerData }) =>
<span>Seconds passed: { timerData.secondsPassed } </span>
);
Copy
可观察的局部组件状态
就像普通类一样,你可以通过使用
@observable
装饰器在React组件上引入可观察属性。 这意味着你可以在组件中拥有功能同样强大的本地状态(local state),而不需要通过 React 的冗长和强制性的setState
机制来管理。 响应式状态会被render
提取调用,但不会调用其它 React 的生命周期方法,除了componentWillUpdate
和componentDidUpdate
。 如果你需要用到其他 React 生命周期方法 ,只需使用基于state
的常规 React API 即可。
上面的例子还可以这样写: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)
Copy
对于使用可观察的局部组件状态更多的优势,请参见为什么我不再使用
setState
的三个理由。使用
inject
将组件连接到提供的 storesmobx-react
包还提供了Provider
组件,它使用了 React 的上下文(context)机制,可以用来向下传递stores
。 要连接到这些 stores,需要传递一个 stores 名称的列表给inject
,这使得 stores 可以作为组件的props
使用。
注意: 从 mobx-react 4开始,注入 stores 的语法发生了变化,应该一直使用inject(stores)(component)
或@inject(stores) class Component...
。 直接传递 store 名称给observer
的方式已废弃。
示例: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都会更新
Copy
更多资料,请参见
mobx-react
文档。何时使用
observer
?简单来说: 所有渲染 observable 数据的组件。 如果你不想将组件标记为 observer,例如为了减少通用组件包的依赖,请确保只传递普通数据。
使用@observer
的话,不再需要从渲染目的上来区分是“智能组件”还是“无脑”组件。 在组件的事件处理、发起请求等方面,它也是一个很好的分离关注点。 当所有组件它们自己的依赖项有变化时,组件自己会响应更新。 而它的计算开销是可以忽略的,并且它会确保不管何时,只要当你开始使用 observable 数据时,组件都将会响应它的变化。 更多详情,请参见 这里。observer
和PureComponent
如果传递给组件的数据是响应式的,
observer
还可以防止当组件的 props 只是浅改变时的重新渲染,这是很有意义的。 这个行为与 React PureComponent 相似,不同在于这里的 state 的更改仍然会被处理。 如果一个组件提供了它自己的shouldComponentUpdate
,这个方法会被优先调用。 想要更详细的解释,请参见这个 github issue。componentWillReact
(生命周期钩子)React 组件通常在新的堆栈上渲染,这使得通常很难弄清楚是什么导致组件的重新渲染。 当使用
mobx-react
时可以定义一个新的生命周期钩子函数componentWillReact
(一语双关)。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候componentWillReact
会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。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>;
}
}
Copy
componentWillReact
不接收参数componentWillReact
初始化渲染前不会触发 (使用componentWillMount
替代)componentWillReact
对于 mobx-react@4+, 当接收新的 props 时并在setState
调用后会触发此钩子优化组件
请参见相关章节。
MobX-React-DevTools
结合
@observer
,可以使用 MobX-React-DevTools ,它精确地显示了何时重新渲染组件,并且可以检查组件的数据依赖关系。 详情请参见 开发者工具 。observer 组件特性
Observer 仅订阅在上次渲染期间活跃使用的数据结构。这意味着你不会订阅不足(under-subscribe)或者过度订阅(over-subscribe)。你甚至可以在渲染方法中使用仅在未来时间段可用的数据。 这是异步加载数据的理想选择。
- 你不需要声明组件将使用什么数据。相反,依赖关系在运行时会确定并以非常细粒度的方式进行跟踪。
- 通常,响应式组件没有或很少有状态,因为在与其他组件共享的对象中封装(视图)状态通常更方便。但你仍然可以自由地使用状态。
@observer
以和PureComponent
同样的方式实现了shouldComponentUpdate
,因此子组件可以避免不必要的重新渲染。- 响应式组件单方面加载数据,即使子组件要重新渲染,父组件也不会进行不必要地重新渲染。
@observer
不依赖于 React 的上下文系统。mobx-react@4+ 中,observer 组件的props 对象和 state 对象都会自动地转变为 observable,这使得创建 @computed 属性更容易,@computed 属性是根据组件内部的 props 推导得到的。如果在
@observer
组件中包含 reaction(例如autorun
) 的话,当 reaction 使用的特定属性不再改变时,reaction 是不会再重新运行的,在 reaction 中使用的特定 props 一定要间接引用(例如const myProp = props.myProp
)。不然,如果你在 reaction 中引用了props.myProp
,那么 props 的任何改变都会导致 reaction 的重新运行。对于 React-Router 的典型用例,请参见这篇文章。在编译器中启用装饰器
在使用 TypeScript 或 Babel 这些等待ES标准定义的编译器时,默认情况下是不支持装饰器的。
对于 typescript,启用
--experimentalDecorators
编译器标识或者在tsconfig.json
中把编译器属性experimentalDecorators
设置为true
(推荐做法)- 对于 babel5,确保把
--stage 0
传递给 Babel CLI - 对于 babel6,参见此 issue 中建议的示例配置。