理解响应式
MobX 通常对您期望它响应的精确事物做出反应,这意味着在 90% 的使用案例中,MobX 应该“正常工作”。然而,在某些时候,您会遇到它没有按预期执行的情况。此时,了解 MobX 如何确定响应内容非常重要。
MobX 对在跟踪函数执行过程中读取的任何现有可观察属性做出反应。
- “读取”是指解引用对象的属性,可以通过“点运算符”(例如
user.name
)或使用方括号表示法(例如user['name']
、todos[3]
)或解构(例如const {name} = user
)来完成。 - “跟踪函数”是指
computed
的表达式、observer
React 函数组件的渲染、基于observer
的 React 类组件的render()
方法,以及作为第一个参数传递给autorun
、reaction
和when
的函数。 - “期间”是指仅跟踪在函数执行时读取的可观察对象。这些值是否被跟踪函数直接或间接使用并不重要。但是从函数中“衍生”出的事物不会被跟踪(例如
setTimeout
、promise.then
、await
等)。
换句话说,MobX 不会对以下内容做出反应:
- 从可观察对象中获取的值,但在跟踪函数之外
- 在异步调用的代码块中读取的可观察对象
MobX 跟踪属性访问,而不是值
为了用示例详细说明上述规则,假设您拥有以下可观察实例
class Message {
title
author
likes
constructor(title, author, likes) {
makeAutoObservable(this)
this.title = title
this.author = author
this.likes = likes
}
updateTitle(title) {
this.title = title
}
}
let message = new Message("Foo", { name: "Michel" }, ["Joe", "Sara"])
在内存中,它看起来如下所示。绿色框表示可观察属性。请注意,值本身不可观察!
MobX 基本上做的是记录您在函数中使用的哪些箭头。之后,只要这些箭头发生变化,它就会重新运行;当它们开始引用其他内容时。
示例
让我们通过一些示例来展示这一点(基于上面定义的 message
变量)
正确:在跟踪函数内解引用
autorun(() => {
console.log(message.title)
})
message.updateTitle("Bar")
这将按预期做出反应。.title
属性被 autorun 解引用,并在之后被更改,因此此更改被检测到。
您可以通过在跟踪函数内部调用 trace()
来验证 MobX 将跟踪什么。对于上述函数,它会输出以下内容
import { trace } from "mobx"
const disposer = autorun(() => {
console.log(message.title)
trace()
})
// Outputs:
// [mobx.trace] 'Autorun@2' tracing enabled
message.updateTitle("Hello")
// Outputs:
// [mobx.trace] 'Autorun@2' is invalidated due to a change in: '[email protected]'
Hello
也可以使用 getDependencyTree
获取内部依赖项(或观察者)树
import { getDependencyTree } from "mobx"
// Prints the dependency tree of the reaction coupled to the disposer.
console.log(getDependencyTree(disposer))
// Outputs:
// { name: 'Autorun@2', dependencies: [ { name: '[email protected]' } ] }
不正确:更改非可观察引用
autorun(() => {
console.log(message.title)
})
message = new Message("Bar", { name: "Martijn" }, ["Felicia", "Marcus"])
这不会做出反应。message
被更改了,但 message
不是可观察对象,只是一个引用可观察对象的变量,但变量(引用)本身不可观察。
不正确:在跟踪函数之外解引用
let title = message.title
autorun(() => {
console.log(title)
})
message.updateMessage("Bar")
这不会做出反应。message.title
在 autorun
之外被解引用,并且只包含解引用时 message.title
的值(字符串 "Foo"
)。title
不是可观察对象,因此 autorun
永远不会做出反应。
正确:在跟踪函数内解引用
autorun(() => {
console.log(message.author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
这将对两种更改做出反应。author
和 author.name
都被点运算符访问,允许 MobX 跟踪这些引用。
请注意,我们必须在这里使用 runInAction
才能被允许在 action
之外进行更改。
不正确:在没有跟踪的情况下存储对可观察对象的本地引用
const author = message.author
autorun(() => {
console.log(author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
第一个更改将被拾取,message.author
和 author
是同一个对象,并且 .name
属性在 autorun 中被解引用。但是,第二个更改不会被拾取,因为 message.author
关系没有被 autorun
跟踪。Autorun 仍然使用“旧”的 author
。
常见陷阱:console.log
autorun(() => {
console.log(message)
})
// Won't trigger a re-run.
message.updateTitle("Hello world")
在上面的示例中,更新后的消息标题不会被打印,因为它没有在 autorun 内部使用。autorun 仅依赖于 message
,它不是可观察对象,而是一个变量。换句话说,就 MobX 而言,title
在 autorun
中没有使用。
如果您在 Web 浏览器调试工具中使用此工具,您可能仍然能够找到 title
的更新值,但这具有误导性 - autorun 最终在它被第一次调用时运行了一次。之所以会发生这种情况,是因为 console.log
是一个异步函数,对象会在稍后的时间被格式化。这意味着,如果您在调试工具栏中跟踪标题,您会找到更新后的值。但 autorun
不会跟踪任何更新。
使其正常工作的方法是确保始终将不可变数据或防御性副本传递给 console.log
。因此,以下解决方案都将对 message.title
的更改做出反应
autorun(() => {
console.log(message.title) // Clearly, the `.title` observable is used.
})
autorun(() => {
console.log(mobx.toJS(message)) // toJS creates a deep clone, and thus will read the message.
})
autorun(() => {
console.log({ ...message }) // Creates a shallow clone, also using `.title` in the process.
})
autorun(() => {
console.log(JSON.stringify(message)) // Also reads the entire structure.
})
正确:在跟踪函数中访问数组属性
autorun(() => {
console.log(message.likes.length)
})
message.likes.push("Jennifer")
这将按预期做出反应。.length
计入属性。请注意,这将对数组中的任何更改做出反应。数组不是按索引/属性(如可观察对象和映射)跟踪的,而是作为一个整体跟踪的。
不正确:在跟踪函数中访问超出界限的索引
autorun(() => {
console.log(message.likes[0])
})
message.likes.push("Jennifer")
这将与上面的示例数据产生反应,因为数组索引计数被视为属性访问。但是,只有在提供的 `index < length` 时才会如此。MobX 不会跟踪尚未存在的数组索引。因此,请始终使用 `length` 检查来保护基于数组索引的访问。
正确:在跟踪函数中访问数组函数
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes.push("Jennifer")
这将按预期产生反应。所有不会更改数组的数组函数都会被自动跟踪。
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes[2] = "Jennifer"
这将按预期产生反应。所有数组索引赋值都会被检测到,但只有在 `index <= length` 时才会如此。
错误:"使用" Observable,但没有访问其任何属性
autorun(() => {
message.likes
})
message.likes.push("Jennifer")
这将不会产生反应。仅仅因为 `likes` 数组本身没有被 `autorun` 使用,只有对数组的引用。因此,相反的是,`message.likes = ["Jennifer"]` 将会被捕获;该语句不会修改数组,而是 `likes` 属性本身。
正确:使用尚未存在的 Map 条目
const twitterUrls = observable.map({
Joe: "twitter.com/joey"
})
autorun(() => {
console.log(twitterUrls.get("Sara"))
})
runInAction(() => {
twitterUrls.set("Sara", "twitter.com/horsejs")
})
这将产生反应。Observable Map 支持观察可能不存在的条目。请注意,这将最初打印 `undefined`。您可以使用 `twitterUrls.has("Sara")` 首先检查条目的存在性。因此,在没有 Proxy 支持动态键值集合的环境中,始终使用 Observable Map。如果您有 Proxy 支持,您也可以使用 Observable Map,但您也可以选择使用普通对象。
MobX 不会跟踪异步访问的数据
function upperCaseAuthorName(author) {
const baseName = author.name
return baseName.toUpperCase()
}
autorun(() => {
console.log(upperCaseAuthorName(message.author))
})
runInAction(() => {
message.author.name = "Chesterton"
})
这将产生反应。即使 `author.name` 没有被传递给 `autorun` 本身的函数取消引用,MobX 仍然会跟踪在 `upperCaseAuthorName` 中发生的取消引用,因为它发生在 `autorun` 执行期间。
autorun(() => {
setTimeout(() => console.log(message.likes.join(", ")), 10)
})
runInAction(() => {
message.likes.push("Jennifer")
})
这将不会产生反应,因为在 `autorun` 执行期间,没有访问 Observable,只有在 `setTimeout` 期间,而 `setTimeout` 是一个异步函数。
查看 异步操作 部分。
使用非 Observable 对象属性
autorun(() => {
console.log(message.author.age)
})
runInAction(() => {
message.author.age = 10
})
如果您在支持 Proxy 的环境中运行 React,这将产生反应。请注意,这仅针对使用 `observable` 或 `observable.object` 创建的对象执行。类实例上的新属性不会自动变为 Observable。
不支持 Proxy 的环境
这将不会产生反应。MobX 只能跟踪 Observable 属性,而 'age' 在上面没有被定义为 Observable 属性。
但是,可以使用 MobX 公开的 `get` 和 `set` 方法来解决这个问题。
import { get, set } from "mobx"
autorun(() => {
console.log(get(message.author, "age"))
})
set(message.author, "age", 10)
[不支持 Proxy] 错误:使用尚未存在的 Observable 对象属性
autorun(() => {
console.log(message.author.age)
})
extendObservable(message.author, {
age: 10
})
这将不会产生反应。MobX 不会对在开始跟踪时不存在的 Observable 属性产生反应。如果两个语句交换位置,或者任何其他 Observable 导致 `autorun` 重新运行,则 `autorun` 将开始跟踪 `age`。
[不支持 Proxy] 正确:使用 MobX 工具读取 / 写入对象
如果您在不支持 Proxy 的环境中,并且仍然希望将 Observable 对象用作动态集合,可以使用 MobX 的 `get` 和 `set` API 来处理它们。
以下内容也将产生反应
import { get, set, observable } from "mobx"
const twitterUrls = observable.object({
Joe: "twitter.com/joey"
})
autorun(() => {
console.log(get(twitterUrls, "Sara")) // `get` can track not yet existing properties.
})
runInAction(() => {
set(twitterUrls, { Sara: "twitter.com/horsejs" })
})
查看 集合工具 API 获取更多详细信息。
TL;DR
MobX 对在跟踪函数执行过程中读取的任何现有可观察属性做出反应。