MobX 会对什么作出反应?

MobX 通常会对你期望的东西做出反应。这意味着在90%的场景下,mobx “都可以工作”。然而,在某些时候,你会遇到一个情况,它可能不会像你所期望的那样工作。在这个时候理解 MobX 如何确定对什么有反应就显得尤为重要。

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

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

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

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

MobX 追踪属性访问,而不是值

用一个示例来阐述上述规则,假设你有如下的 observable 数据结构(默认情况下 observable 会递归应用,所以本示例中的所有字段都是可观察的)。

  1. let message = observable({
  2. title: "Foo",
  3. author: {
  4. name: "Michel"
  5. },
  6. likes: [
  7. "John", "Sara"
  8. ]
  9. })

在内存中看起来像下面这样。 绿色框表示可观察属性。 请注意, 本身是不可观察的!

MobX reacts to changing references

现在 MobX 基本上所做的是记录你在函数中使用的是哪个箭头。之后,只要这些箭头中的其中一个改变了(它们开始引用别的东西了),它就会重新运行。

示例

来看下下面这些示例(基于上面定义的 message 变量):

正确的: 在追踪函数内进行间接引用

  1. autorun(() => {
  2. console.log(message.title)
  3. })
  4. message.title = "Bar"

这将如预期一样会作出反应,.title 属性会被 autorun 间接引用并且在之后发生了改变,所以这个改变是能检测到的。

你可以通过在追踪函数内调用 trace() 方法来验证 MobX 在追踪什么。以上面的函数为例,输出结果如下:

  1. const disposer = autorun(() => {
  2. console.log(message.title)
  3. trace()
  4. })
  5. // 输出:
  6. // [mobx.trace] 'Autorun@2' tracing enabled
  7. message.title = "Hello"
  8. // [mobx.trace] 'Autorun@2' is invalidated due to a change in: 'ObservableObject@1.title'

还可以通过使用指定工具来获取内部的依赖 (或观察者):

  1. getDependencyTree(disposer) // 输出与 disposer 耦合的 reaction 的依赖树
  2. // { name: 'Autorun@4',
  3. // dependencies: [ { name: 'ObservableObject@1.title' } ] }

错误的: 改变了非 observable 的引用

  1. autorun(() => {
  2. console.log(message.title)
  3. })
  4. message = observable({ title: "Bar" })

这将不会作出反应。message 被改变了,但它不是 observable,它只是一个引用 observable 的变量,但是变量(引用)本身并不是可观察的。

错误的: 在追踪函数外进行间接引用

  1. var title = message.title;
  2. autorun(() => {
  3. console.log(title)
  4. })
  5. message.title = "Bar"

这将不会作出反应。message.title 是在 autorun 外面进行的间接引用,在间接引用的时候 title 变量只是包含 message.title 的值(字符串 Foo)而已。title 变量不是 observable,所以 autorun 永远不会作出反应。

正确的: 在追踪函数内进行间接引用

  1. autorun(() => {
  2. console.log(message.author.name)
  3. })
  4. message.author.name = "Sara";
  5. message.author = { name: "John" };

对于这两个变化都将作出反应。 authorauthor.name 都是通过 . 访问的,使得 MobX 可以追踪这些引用。

错误的: 存储 observable 对象的本地引用而不对其追踪

  1. const author = message.author;
  2. autorun(() => {
  3. console.log(author.name)
  4. })
  5. message.author.name = "Sara";
  6. message.author = { name: "John" };

对于第一个改变将会作出反应,message.authorauthor 是同一个对象,而 name 属性在 autorun 中进行的间接引用。但对于第二个改变将不会作出反应,message.author 的关系没有通过 autorun 追踪。Autorun 仍然使用的是“老的” author

常见陷阱: console.log

  1. const message = observable({ title: "hello" })
  2. autorun(() => {
  3. console.log(message)
  4. })
  5. // 不会触发重新运行
  6. message.title = "Hello world"

在上面的示例中,更新 messagetitle 属性不会被打印出来,因为没有在 autorun 内使用。autorun 只依赖于 message,它不是 observable,而是常量。换句话说,对于 MobX 而言,它没有使用 title,因此与 autorun 无关。

事实上 console.log 会打印出 messagetitle,这是让人费解的,console.log 是异步 API,它只会稍后对参数进行格式化,因此 autorun 不会追踪 console.log 访问的数据。所以,请确保始终传递不变数据 ( immutable data ) 或防御副本给 console.log

下面是一些解决方案,它们会对 message.title 作出反应:

  1. autorun(() => {
  2. console.log(message.title) // 很显然, 使用了 `.title` observable
  3. })
  4. autorun(() => {
  5. console.log(mobx.toJS(message)) // toJS 创建了深克隆,从而读取消息
  6. })
  7. autorun(() => {
  8. console.log({...message}) // 创建了浅克隆,在此过程中也使用了 `.title`
  9. })
  10. autorun(() => {
  11. console.log(JSON.stringify(message)) // 读取整个结构
  12. })

正确的: 在追踪函数内访问数组属性

  1. autorun(() => {
  2. console.log(message.likes.length);
  3. })
  4. message.likes.push("Jennifer");

这将如预期一样会作出反应。.length 指向一个属性。注意这会对数组中的任何更改做出反应。数组不追踪每个索引/属性(如 observable 对象和映射),而是将其作为一个整体追踪。

错误的: 在追踪函数内索引越界访问

  1. autorun(() => {
  2. console.log(message.likes[0]);
  3. })
  4. message.likes.push("Jennifer");

使用上面的示例数据是会作出反应的,数组的索引计数作为属性访问,但前提条件必须是提供的索引小于数组长度。MobX 不会追踪还不存在的索引或者对象属性(当使用 observable 映射(map)时除外)。所以建议总是使用 .length 来检查保护基于数组索引的访问。

正确的: 在追踪函数内访问数组方法

  1. autorun(() => {
  2. console.log(message.likes.join(", "));
  3. })
  4. message.likes.push("Jennifer");

这将如预期一样会作出反应。所有不会改变数组的数组方法都会自动地追踪。


  1. autorun(() => {
  2. console.log(message.likes.join(", "));
  3. })
  4. message.likes[2] = "Jennifer";

这将如预期一样会作出反应。所有数组的索引分配都可以检测到,但前提条件必须是提供的索引小于数组长度。

错误的: “使用” observable 但没有访问它的任何属性

  1. autorun(() => {
  2. message.likes;
  3. })
  4. message.likes.push("Jennifer");

这将不会作出反应。只是因为 likes 数组本身并没有被 autorun 使用,只是引用了数组。所以相比之下,messages.likes = ["Jennifer"] 是会作出反应的,表达式没有修改数组,而是修改了 likes 属性本身。

使用对象的非 observable 属性

  1. autorun(() => {
  2. console.log(message.postDate)
  3. })
  4. message.postDate = new Date()

MobX 4

这将不会作出反应。MobX 只能追踪 observable 属性,上面的 postDate 还未被定义为 observable 属性。但是,仍然可以使用 MobX 提供的 getset 方法来使其工作:

  1. autorun(() => {
  2. console.log(get(message, "postDate"))
  3. })
  4. set(message, "postDate", new Date())

MobX 5

在 MobX 5 中是作出反应的,因为 MobX 5 可以追踪还不存在的属性。注意,这只适用于由 observableobservable.object 创建出的对象。对于类实例上的新属性,还是无法自动将其变成 observable 的。

[MobX 4 及以下版本] 错误的: 使用 observable 对象还不存在的属性

  1. autorun(() => {
  2. console.log(message.postDate)
  3. })
  4. extendObservable(message, {
  5. postDate: new Date()
  6. })

这将不会作出反应。MobX 不会对当追踪开始时还不能存在的 observable 属性作出反应。如果两个表达式交换下顺序,或者任何其它的 observable 引起 autorun 再次运行的话,autorun 也会开始追踪 postDate 属性了。

正确的: 使用映射中还不存在的项

  1. const twitterUrls = observable.map({
  2. "John": "twitter.com/johnny"
  3. })
  4. autorun(() => {
  5. console.log(twitterUrls.get("Sara"))
  6. })
  7. twitterUrls.set("Sara", "twitter.com/horsejs")

这将作出反应。Observable 映射支持观察还不存在的项。注意这里最初会输出 undefined。可以通过使用 twitterUrls.has("Sara") 来先检查该项是否存在。所以对于动态键集合,总是使用 observable 映射。

正确的: 使用 MobX 工具来读/写对象

MobX 4 之后还可以将 observable 对象当做动态集合使用,如果使用 MobX API 来进行读/更新操作,那么 MobX 可以追踪属性的变化。下面的代码同样可以进行反应:

  1. import { get, set, observable } from "mobx"
  2. const twitterUrls = observable.object({
  3. "John": "twitter.com/johnny"
  4. })
  5. autorun(() => {
  6. console.log(get(twitterUrls, "Sara")) // get 可以追踪还未创建的属性
  7. })
  8. set(twitterUrls, { "Sara" : "twitter.com/horsejs"})

想了解更多,请参见 对象操作 API

MobX 只追踪同步地访问数据

  1. function upperCaseAuthorName(author) {
  2. const baseName = author.name;
  3. return baseName.toUpperCase();
  4. }
  5. autorun(() => {
  6. console.log(upperCaseAuthorName(message.author))
  7. })
  8. message.author.name = "Chesterton"

这将作出反应。尽管 author.name 不是在 autorun 本身的代码块中进行直接引用的。MobX 会追踪发生在 upperCaseAuthorName 函数里的间接引用,因为它是在 autorun 执行期间发生的。


  1. autorun(() => {
  2. setTimeout(
  3. () => console.log(message.likes.join(", ")),
  4. 10
  5. )
  6. })
  7. message.likes.push("Jennifer");

这将不会作出反应。在 autorun 执行期间没有访问到任何 observable,而只在 setTimeout 执行期间访问了。通常来说,这是相当明显的,很少会导致问题。

MobX 只会为数据是直接通过 render 存取的 observer 组件进行数据追踪

一个使用 observer 的常见错误是它不会追踪语法上看起来像 observer 父组件的数据,但实际上是由不同的组件渲染的。当组件的 render 回调函数在第一个类中传递给另一个组件时,经常会发生这种情况。

看下面这个人造的示例:

  1. const MyComponent = observer(({ message }) =>
  2. <SomeContainer
  3. title = {() => <div>{message.title}</div>}
  4. />
  5. )
  6. message.title = "Bar"

起初看上去一切似乎都是没问题的,除了 <div> 实际上不是由 MyComponent(有追踪的渲染) 渲染的,而是 SomeContainer。所以要确保 SomeContainer 的 title 可以正确对新的 message.title 作出反应,SomeContainer 应该也是一个 observer

如果 SomeContainer 来源于外部库的话,这通常不在你的掌控之中。在这种场景下,你可以用自己的无状态 observer 组件来包裹 div 解决此问题,或通过利用 <Observer>组件:

  1. const MyComponent = observer(({ message }) =>
  2. <SomeContainer
  3. title = {() => <TitleRenderer message={message} />}
  4. />
  5. )
  6. const TitleRenderer = observer(({ message }) =>
  7. <div>{message.title}</div>}
  8. )
  9. message.title = "Bar"

另外一种方法可以避免创建额外组件,它同样适用了 mobx-react 内置的 Observer 组件,它不接受参数,只需要单个的 render 函数作为子节点:

  1. const MyComponent = ({ message }) =>
  2. <SomeContainer
  3. title = {() =>
  4. <Observer>
  5. {() => <div>{message.title}</div>}
  6. </Observer>
  7. }
  8. />
  9. message.title = "Bar"

避免在本地字段中缓存 observable

一个常见的错误就是把间接引用的 observable 存储到本地变量,然后认为组件会作出反应。举例来说:

  1. @observer class MyComponent extends React.component {
  2. author;
  3. constructor(props) {
  4. super(props)
  5. this.author = props.message.author;
  6. }
  7. render() {
  8. return <div>{this.author.name}</div>
  9. }
  10. }

组件会对 author.name 的变化作出反应,但不会对 message 本身的 .author 的变化作出反应!因为这个间接引用发生在 render() 之外,而render()observer 组件的唯一追踪函数。注意,即便把组件的 author 字段标记为 @observable 字段也不能解决这个问题,author 仍然是只分配一次。这个问题可以简单地解决,方法是在 render() 中进行间接引用或者在组件实例上引入一个计算属性:

  1. @observer class MyComponent extends React.component {
  2. @computed get author() {
  3. return this.props.message.author
  4. }
  5. // ...

多个组件将如何渲染

假设下面的组件是用来渲染上面的 message 对象的。

  1. const Message = observer(({ message }) =>
  2. <div>
  3. {message.title}
  4. <Author author={ message.author } />
  5. <Likes likes={ message.likes } />
  6. </div>
  7. )
  8. const Author = observer(({ author }) =>
  9. <span>{author.name}</span>
  10. )
  11. const Likes = observer(({ likes }) =>
  12. <ul>
  13. {likes.map(like =>
  14. <li>{like}</li>
  15. )}
  16. </ul>
  17. )
变化 重新渲染组件
message.title = "Bar" Message
message.author.name = "Susan" Author (.authorMessage 中进行间接引用, 但没有改变)*
message.author = { name: "Susan"} Message, Author
message.likes[0] = "Michel" Likes

注意:

  1. * 如果 Author 组件是像这样调用的: <Author author={ message.author.name} />Message 会是进行间接引用的组件并对 message.author.name 的改变作出反应。尽管如此,<Author> 同样会重新渲染,因为它接收到了一个新的值。所以从性能上考虑,越晚进行间接引用越好。
  2. ** 如果 likes 数组里面的是对象而不是字符串,并且它们在它们自己的 Like 组件中渲染,那么对于发生在某个具体的 like 中发生的变化,Likes 组件将不会重新渲染。

TL;DR

MobX 会对在执行跟踪函数期间读取的任何现有的可观察属性做出反应。