Computed的使用

computed计算属性,依赖缓存
如果任何影响计算值的值发生变化了,计算值将根据状态自动进行衍生。 计算值在大多数情况下可以被 MobX 优化的,因为它们被认为是纯函数。 例如,如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。 如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。 在这种情况下,它将被暂停。
这个自动地暂停是非常方便的。如果一个计算值不再被观察了,例如使用它的UI不复存在了,MobX 可以自动地将其垃圾回收。而 autorun 中的值必须要手动清理才行,这点和计算值是有所不同的。 如果你创建一个计算属性,但不在 reaction 中的任何地方使用它,它不会缓存值并且有些重新计算看起来似乎是没有必要的。这点有时候会让刚接触 MobX 的人们很困惑。 然而,在现实开发场景中,这是迄今为止最好的默认逻辑。如果你需要的话,可以使用 observekeepAlive 来强制保持计算值总是处于唤醒状态。
注意计算属性是不可枚举的,它们也不能在继承链中被覆盖。

  1. import {observable, computed} from "mobx";
  2. class OrderLine {
  3. @observable price = 0;
  4. @observable amount = 1;
  5. constructor(price) {
  6. this.price = price;
  7. }
  8. @computed get total() {
  9. return this.price * this.amount;
  10. }
  11. }
  12. import {decorate, observable, computed} from "mobx";
  13. class OrderLine {
  14. price = 0;
  15. amount = 1;
  16. constructor(price) {
  17. this.price = price;
  18. }
  19. get total() {
  20. return this.price * this.amount;
  21. }
  22. }
  23. decorate(OrderLine, {
  24. price: observable,
  25. amount: observable,
  26. total: computed
  27. })
  28. // observable.object 和 extendObservable
  29. // 都会自动将 getter 属性推导成计算属性,所以下面这样就足够了
  30. const orderLine = observable.object({
  31. price: 0,
  32. amount: 1,
  33. get total() {
  34. return this.price * this.amount
  35. }
  36. })
  37. const orderLine = observable.object({
  38. price: 0,
  39. amount: 1,
  40. get total() {
  41. return this.price * this.amount
  42. },
  43. set total(total) {
  44. this.price = total / this.amount // 从 total 中推导出 price
  45. }
  46. })
  47. class Foo {
  48. @observable length = 2;
  49. @computed get squared() {
  50. return this.length * this.length;
  51. }
  52. set squared(value) { // 这是一个自动的动作,不需要注解
  53. this.length = Math.sqrt(value);
  54. }
  55. }

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 ,并将计算值保存在内存中!

@computed.struct 用于比较结构

@computed 装饰器不需要接收参数。如果你想创建一个能进行结构比较的计算属性时,请使用 @computed.struct

内置比较器

MobX 提供了三个内置 comparer (比较器) ,它们应该能满足绝大部分需求:

  • comparer.identity: 使用恒等 (===) 运算符来判定两个值是否相同。
  • comparer.default: 等同于 comparer.identity,但还认为 NaN 等于 NaN
  • comparer.structural: 执行深层结构比较以确定两个值是否相同。

    computed错误处理

  1. const x = observable.box(3)
  2. const y = observable.box(1)
  3. const divided = computed(() => {
  4. if (y.get() === 0)
  5. throw new Error("Division by zero")
  6. return x.get() / y.get()
  7. })
  8. divided.get() // 返回 3
  9. y.set(0) // OK
  10. divided.get() // 报错: Division by zero
  11. divided.get() // 报错: Division by zero
  12. y.set(2)
  13. divided.get() // 已恢复; 返回 1.5

Autorun

当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun。 这通常是当你需要从反应式代码桥接到命令式代码的情况,例如打印日志、持久化或者更新UI的代码。 当使用 autorun时,所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。

经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun。 其余情况都应该使用 computed。 Autoruns 是关于 启动效果 (initiating effects) 的 ,而不是产生新的值。 如果字符串作为第一个参数传递给 autorun ,它将被用作调试名。
传递给 autorun 的函数在调用后将接收一个参数,即当前 reaction(autorun),可用于在执行期间清理 autorun。
就像 @ observer 装饰器/函数autorun 只会观察在执行提供的函数时所使用的数据。

  1. var numbers = observable([1,2,3]);
  2. var sum = computed(() => numbers.reduce((a, b) => a + b, 0));
  3. var disposer = autorun(() => console.log(sum.get()));
  4. // 输出 '6'
  5. numbers.push(4);
  6. // 输出 '10'
  7. disposer();
  8. numbers.push(5);
  9. // 不会再输出任何值。`sum` 不会再重新计算。

选项

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

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

    delay 选项

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

    onError 选项

    在 autorun 和所有其他类型 reaction 中抛出的异常会被捕获并打印到控制台,但不会将异常传播回原始导致异常的代码。 这是为了确保一个异常中的 reaction 不会阻止其他可能不相关的 reaction 的预定执行。 这也允许 reaction 从异常中恢复; 抛出异常不会破坏 MobX的跟踪,因此如果除去异常的原因,reaction 的后续运行可能会再次正常完成。
    可以通过提供 onError 选项来覆盖 Reactions 的默认日志行为。 示例:
    1. const age = observable.box(10)
    2. const dispose = autorun(() => {
    3. if (age.get() < 0)
    4. throw new Error("Age should not be negative")
    5. console.log("Age", age.get())
    6. }, {
    7. onError(e) {
    8. window.alert("Please enter a valid age")
    9. }
    10. })
    11. Copy
    一个全局的 onError 处理方法可以使用 onReactionError(handler) 来设置。这在测试或监控中很有用。

    When

    when(predicate: () => boolean, effect?: () => void, options?)
    when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。
    对于以响应式方式来进行处理或者取消,此函数非常有用。 示例:
    1. class MyResource {
    2. constructor() {
    3. when(
    4. // 一旦...
    5. () => !this.isVisible,
    6. // ... 然后
    7. () => this.dispose()
    8. );
    9. }
    10. @computed get isVisible() {
    11. // 标识此项是否可见
    12. }
    13. dispose() {
    14. // 清理
    15. }
    16. }
    17. Copy

    when-promise

    如果没提供 effect 函数,when 会返回一个 Promise 。它与 async / await 可以完美结合。
    1. async function() {
    2. await when(() => that.isVisible)
    3. // 等等..
    4. }

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 函数的重新运行

    示例

    在下面的示例中,reaction1reaction2autorun1 都会对 todos 数组中的 todo 的添加、删除或替换作出反应。 但只有 reaction2autorun 会对某个 todo 的 title 变化作出反应,因为在 reaction2 的数据表达式中使用了 title,而 reaction1 的数据表达式没有使用。 autorun 追踪完整的副作用,因此它将始终正确触发,但也更容易意外地访问相关数据。 还可参见 MobX 会对什么作出反应?.

    1. const todos = observable([
    2. {
    3. title: "Make coffee",
    4. done: true,
    5. },
    6. {
    7. title: "Find biscuit",
    8. done: false
    9. }
    10. ]);
    11. // reaction 的错误用法: 对 length 的变化作出反应, 而不是 title 的变化!
    12. const reaction1 = reaction(
    13. () => todos.length,
    14. length => console.log("reaction 1:", todos.map(todo => todo.title).join(", "))
    15. );
    16. // reaction 的正确用法: 对 length 和 title 的变化作出反应
    17. const reaction2 = reaction(
    18. () => todos.map(todo => todo.title),
    19. titles => console.log("reaction 2:", titles.join(", "))
    20. );
    21. // autorun 对它函数中使用的任何东西作出反应
    22. const autorun1 = autorun(
    23. () => console.log("autorun 1:", todos.map(todo => todo.title).join(", "))
    24. );
    25. todos.push({ title: "explain reactions", done: false });
    26. // 输出:
    27. // reaction 1: Make coffee, find biscuit, explain reactions
    28. // reaction 2: Make coffee, find biscuit, explain reactions
    29. // autorun 1: Make coffee, find biscuit, explain reactions
    30. todos[0].title = "Make tea"
    31. // 输出:
    32. // reaction 2: Make tea, find biscuit, explain reactions
    33. // autorun 1: Make tea, find biscuit, explain reactions
    34. Copy

    在下面的示例中,reaction3 会对 counter 中的 count 作出反应。 当调用 reaction 时,第二个参数会作为清理函数使用。 下面的示例展示了 reaction 只会调用一次。

    1. const counter = observable({ count: 0 });
    2. // 只调用一次并清理掉 reaction : 对 observable 值作出反应。
    3. const reaction3 = reaction(
    4. () => counter.count,
    5. (count, reaction) => {
    6. console.log("reaction 3: invoked. counter.count = " + count);
    7. reaction.dispose();
    8. }
    9. );
    10. counter.count = 1;
    11. // 输出:
    12. // reaction 3: invoked. counter.count = 1
    13. counter.count = 2;
    14. // 输出:
    15. // (There are no logging, because of reaction disposed. But, counter continue reaction)
    16. console.log(counter.count);
    17. // 输出:
    18. // 2
    19. Copy

    粗略地讲,reaction 是 computed(expression).observe(action(sideEffect))autorun(() => action(sideEffect)(expression)) 的语法糖。

    @observer

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

    1. import {observer} from "mobx-react";
    2. var timerData = observable({
    3. secondsPassed: 0
    4. });
    5. setInterval(() => {
    6. timerData.secondsPassed++;
    7. }, 1000);
    8. @observer class Timer extends React.Component {
    9. render() {
    10. return (<span>Seconds passed: { this.props.timerData.secondsPassed } </span> )
    11. }
    12. };
    13. ReactDOM.render(<Timer timerData={timerData} />, document.body);

    小贴士: 当 observer 需要组合其它装饰器或高阶组件时,请确保 observer 是最深处(第一个应用)的装饰器,否则它可能什么都不做。
    注意,使用 @observer 装饰器是可选的,它和 observer(class Timer ... { }) 达到的效果是一样的。

    陷阱: 组件中的间接引用值

    MobX 可以做很多事,但是它无法使原始数据类型值转变成可观察的(尽管它可以用对象来包装它们,参见 boxed observables)。 所以是不可观察的,但是对象的属性可以。这意味着 @observer 实际上是对间接引用(dereference)值的反应。 那么在上面的示例中,如果是用下面这种方式初始化的,Timer 组件是不会有反应的:

    1. React.render(<Timer timerData={timerData.secondsPassed} />, document.body)

    在这个代码片段中只是把 secondsPassed 的当前值传递给了 Timer 组件,这个值是不可变值0(JS中所有的原始类型值都是不可变的)。 这个数值永远都不会改变,因此 Timer 组件不会更新。secondsPassed的值将来会发生改变, 所以我们需要在组件访问它。或者换句话说: 值需要通过引用来传递而不是通过(字面量)值来传递。

    ES5 支持

    在ES5环境中,可以简单地使用 observer(React.createClass({ ... 来定义观察者组件。还可以参见语法指南

    无状态函数组件

    上面的 Timer 组件还可以通过使用 observer 传递的无状态函数组件来编写:

    1. import {observer} from "mobx-react";
    2. const Timer = observer(({ timerData }) =>
    3. <span>Seconds passed: { timerData.secondsPassed } </span>
    4. );
    5. Copy

    可观察的局部组件状态

    就像普通类一样,你可以通过使用 @observable 装饰器在React组件上引入可观察属性。 这意味着你可以在组件中拥有功能同样强大的本地状态(local state),而不需要通过 React 的冗长和强制性的 setState 机制来管理。 响应式状态会被 render 提取调用,但不会调用其它 React 的生命周期方法,除了 componentWillUpdatecomponentDidUpdate 。 如果你需要用到其他 React 生命周期方法 ,只需使用基于 state 的常规 React API 即可。
    上面的例子还可以这样写:

    1. import {observer} from "mobx-react"
    2. import {observable} from "mobx"
    3. @observer class Timer extends React.Component {
    4. @observable secondsPassed = 0
    5. componentWillMount() {
    6. setInterval(() => {
    7. this.secondsPassed++
    8. }, 1000)
    9. }
    10. render() {
    11. return (<span>Seconds passed: { this.secondsPassed } </span> )
    12. }
    13. }
    14. ReactDOM.render(<Timer />, document.body)
    15. Copy

    对于使用可观察的局部组件状态更多的优势,请参见为什么我不再使用 setState 的三个理由

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

    mobx-react 包还提供了 Provider 组件,它使用了 React 的上下文(context)机制,可以用来向下传递 stores。 要连接到这些 stores,需要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用。
    注意: 从 mobx-react 4开始,注入 stores 的语法发生了变化,应该一直使用 inject(stores)(component)@inject(stores) class Component...。 直接传递 store 名称给 observer 的方式已废弃。
    示例:

    1. const colors = observable({
    2. foreground: '#000',
    3. background: '#fff'
    4. });
    5. const App = () =>
    6. <Provider colors={colors}>
    7. <app stuff... />
    8. </Provider>;
    9. const Button = inject("colors")(observer(({ colors, label, onClick }) =>
    10. <button style={{
    11. color: colors.foreground,
    12. backgroundColor: colors.background
    13. }}
    14. onClick={onClick}
    15. >{label}</button>
    16. ));
    17. // 稍后..
    18. colors.foreground = 'blue';
    19. // 所有button都会更新
    20. Copy

    更多资料,请参见 mobx-react 文档

    何时使用 observer?

    简单来说: 所有渲染 observable 数据的组件。 如果你不想将组件标记为 observer,例如为了减少通用组件包的依赖,请确保只传递普通数据。
    使用 @observer 的话,不再需要从渲染目的上来区分是“智能组件”还是“无脑”组件。 在组件的事件处理、发起请求等方面,它也是一个很好的分离关注点。 当所有组件它们自己的依赖项有变化时,组件自己会响应更新。 而它的计算开销是可以忽略的,并且它会确保不管何时,只要当你开始使用 observable 数据时,组件都将会响应它的变化。 更多详情,请参见 这里

    observerPureComponent

    如果传递给组件的数据是响应式的,observer 还可以防止当组件的 props 只是浅改变时的重新渲染,这是很有意义的。 这个行为与 React PureComponent 相似,不同在于这里的 state 的更改仍然会被处理。 如果一个组件提供了它自己的 shouldComponentUpdate,这个方法会被优先调用。 想要更详细的解释,请参见这个 github issue

    componentWillReact (生命周期钩子)

    React 组件通常在新的堆栈上渲染,这使得通常很难弄清楚是什么导致组件的重新渲染。 当使用 mobx-react 时可以定义一个新的生命周期钩子函数 componentWillReact(一语双关)。当组件因为它观察的数据发生了改变,它会安排重新渲染,这个时候 componentWillReact 会被触发。这使得它很容易追溯渲染并找到导致渲染的操作(action)。

    1. import {observer} from "mobx-react";
    2. @observer class TodoView extends React.Component {
    3. componentWillReact() {
    4. console.log("I will re-render, since the todo has changed!");
    5. }
    6. render() {
    7. return <div>this.props.todo.title</div>;
    8. }
    9. }
    10. 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 中建议的示例配置。