了解反应性
¥Understanding reactivity
MobX 通常会对你期望的事情做出准确的反应,这意味着在 90% 的用例中 MobX 应该 "只是工作"。然而,在某些时候你会遇到一种情况,它没有达到你的预期。在这一点上,了解 MobX 如何确定对什么做出反应是非常有价值的。
¥MobX usually reacts to exactly the things you expect it to, which means that in 90% of your use cases MobX should "just work". However, at some point you will encounter a case where it does not do what you expected. At that point it is invaluable to understand how MobX determines what to react to.
MobX 对在跟踪函数执行期间读取的任何现有可观察属性做出反应。
¥MobX reacts to any existing observable property that is read during the execution of a tracked function.
"reading" 正在取消引用对象的属性,这可以通过 "点入" 它(例如
user.name
)或使用括号表示法(例如user['name']
、todos[3]
)或解构(例如const {name} = user
)来完成。¥"reading" is dereferencing an object's property, which can be done through "dotting into" it (eg.
user.name
) or using the bracket notation (eg.user['name']
,todos[3]
) or destructuring (eg.const {name} = user
)."跟踪功能" 是
computed
的表达式,observer
React 函数组件的渲染,基于observer
的 React 类组件的render()
方法,以及作为第一个参数传递给autorun
、reaction
和when
的函数。¥"tracked functions" are the expression of
computed
, the rendering of anobserver
React function component, therender()
method of anobserver
based React class component, and the functions that are passed as the first param toautorun
,reaction
andwhen
."during" 表示仅跟踪函数执行时读取的那些可观察值。跟踪函数直接或间接使用这些值并不重要。但函数中 'spawned' 的内容将不会被跟踪(例如
setTimeout
、promise.then
、await
等)。¥"during" means that only those observables that are read while the function is executing are tracked. It doesn't matter whether these values are used directly or indirectly by the tracked function. But things that have been 'spawned' from the function won't be tracked (e.g.
setTimeout
,promise.then
,await
etc).
换句话说,MobX 不会对以下情况做出反应:
¥In other words, MobX will not react to:
从可观察量获取但在跟踪函数之外的值
¥Values that are obtained from observables, but outside a tracked function
在异步调用的代码块中读取的可观察量
¥Observables that are read in an asynchronously invoked code block
MobX 跟踪属性访问,而不是值
¥MobX tracks property access, not values
为了通过示例来详细说明上述规则,假设你有以下可观察实例:
¥To elaborate on the above rules with an example, suppose that you have the following observable instance:
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"])
在内存中,这看起来如下。绿色框表示可观察的属性。请注意,值本身是不可观察的!
¥In memory this looks as follows. The green boxes indicate observable properties. Note that the values themselves are not observable!
MobX 基本上所做的就是记录你在函数中使用的箭头。之后,只要这些箭头之一发生变化,它就会重新运行;当他们开始提及其他事物时。
¥What MobX basically does is recording which arrows you use in your function. After that, it will re-run whenever one of these arrows changes; when they start to refer to something else.
示例
¥Examples
让我们用一堆例子来展示这一点(基于上面定义的 message
变量):
¥Let's show that with a bunch of examples (based on the message
variable defined above):
正确的:跟踪函数内部取消引用
¥Correct: dereference inside the tracked function
autorun(() => {
console.log(message.title)
})
message.updateTitle("Bar")
这将按预期做出反应。.title
属性由自动运行取消引用,并随后发生更改,因此会检测到此更改。
¥This will react as expected. The .title
property was dereferenced by the autorun, and changed afterwards, so this change is detected.
你可以通过在跟踪函数内调用 trace()
来验证 MobX 将跟踪的内容。对于上述函数,它输出以下内容:
¥You can verify what MobX will track by calling trace()
inside the tracked function. In the case of the above function it outputs the following:
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: 'Message@1.title'
Hello
还可以使用 getDependencyTree
获取内部依赖(或观察者)树:
¥It is also possible to get the internal dependency (or observer) tree by using 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: 'Message@1.title' } ] }
错误的:改变不可观察的参考
¥Incorrect: changing a non-observable reference
autorun(() => {
console.log(message.title)
})
message = new Message("Bar", { name: "Martijn" }, ["Felicia", "Marcus"])
这样就不会反应了。message
被更改,但 message
不是可观察的,只是一个引用可观察的变量,但变量(引用)本身不是可观察的。
¥This will not react. message
was changed, but message
is not an observable, just a variable which refers to an observable, but the variable (reference) itself is not observable.
错误的:在跟踪函数之外取消引用
¥Incorrect: dereference outside of a tracked function
let title = message.title
autorun(() => {
console.log(title)
})
message.updateMessage("Bar")
这样就不会反应了。message.title
在 autorun
之外取消引用,并且仅包含取消引用时 message.title
的值(字符串 "Foo"
)。title
不是可观察的,因此 autorun
永远不会做出反应。
¥This will not react. message.title
was dereferenced outside of autorun
, and just contains the value of message.title
at the moment of dereferencing (the string "Foo"
). title
is not an observable so autorun
will never react.
正确的:跟踪函数内部取消引用
¥Correct: dereference inside the tracked function
autorun(() => {
console.log(message.author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
这对这两种变化都有反应。author
和 author.name
都被点缀进去,允许 MobX 跟踪这些引用。
¥This reacts to both changes. Both author
and author.name
are dotted into, allowing MobX to track these references.
请注意,我们必须在此处使用 runInAction
才能允许在 action
之外进行更改。
¥Note that we had to use runInAction
here to be allowed to make changes outside
of an action
.
错误的:存储对可观察对象的本地引用而不进行跟踪
¥Incorrect: store a local reference to an observable object without tracking
const author = message.author
autorun(() => {
console.log(author.name)
})
runInAction(() => {
message.author.name = "Sara"
})
runInAction(() => {
message.author = { name: "Joe" }
})
将拾取第一个更改,message.author
和 author
是同一对象,并且 .name
属性在自动运行中取消引用。然而,第二个更改没有被拾取,因为 message.author
关系没有被 autorun
跟踪。Autorun 仍然使用 "old" author
。
¥The first change will be picked up, message.author
and author
are the same object, and the .name
property is dereferenced in the autorun.
However, the second change is not picked up, because the message.author
relation is not tracked by the autorun
. Autorun is still using the "old" author
.
常见陷阱:console.log
¥Common pitfall: console.log
autorun(() => {
console.log(message)
})
// Won't trigger a re-run.
message.updateTitle("Hello world")
在上面的示例中,更新的消息标题不会被打印,因为它没有在自动运行中使用。自动运行仅依赖于 message
,它不是一个可观察对象,而是一个变量。也就是说,就 MobX 而言,autorun
中并没有使用 title
。
¥In the above example, the updated message title won't be printed, because it is not used inside the autorun.
The autorun only depends on message
, which is not an observable, but a variable. In other words, as far as MobX is concerned, title
is not used in the autorun
.
如果你在网页浏览器调试工具中使用它,你也许能够找到 title
的更新值,但这是误导性的 - autorun run 毕竟在第一次调用时已经运行过一次。发生这种情况是因为 console.log
是一个异步函数,并且该对象仅在稍后才被格式化。这意味着如果你按照调试工具栏中的标题操作,就可以找到更新后的值。但 autorun
不跟踪任何更新。
¥If you use this in a web browser debugging tool, you may be able to find the
updated value of title
after all, but this is misleading -- autorun run after all has run once when it was first called. This happens because console.log
is an asynchronous function and the object is only formatted later in time. This means that if you follow the title in the debugging toolbar, you can find the updated value. But the autorun
does not track any updates.
实现此目的的方法是确保始终将不可变数据或防御副本传递给 console.log
。因此以下解决方案都对 message.title
中的更改做出反应:
¥The way to make this work is to make sure to always pass immutable data or defensive copies to console.log
. So the following solutions all react to changes in 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.
})
正确的:访问跟踪函数中的数组属性
¥Correct: access array properties in tracked function
autorun(() => {
console.log(message.likes.length)
})
message.likes.push("Jennifer")
这将按预期做出反应。.length
计入属性。请注意,这将对数组中的任何更改做出反应。数组不是按索引/属性(如可观察对象和映射)进行跟踪,而是作为一个整体进行跟踪。
¥This will react as expected. .length
counts towards a property.
Note that this will react to any change in the array.
Arrays are not tracked per index / property (like observable objects and maps), but as a whole.
错误的:访问跟踪函数中的越界索引
¥Incorrect: access out-of-bounds indices in tracked function
autorun(() => {
console.log(message.likes[0])
})
message.likes.push("Jennifer")
这将与上面的示例数据发生反应,因为数组索引算作属性访问。但前提是提供了 index < length
。MobX 不跟踪尚不存在的数组索引。因此,请始终使用 .length
检查来保护基于数组索引的访问。
¥This will react with the above sample data because array indexes count as property access. But only if the provided index < length
.
MobX does not track not-yet-existing array indices.
So always guard your array index based access with a .length
check.
正确的:访问跟踪函数中的数组函数
¥Correct: access array functions in tracked function
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes.push("Jennifer")
这将按预期做出反应。所有不会改变数组的数组函数都会被自动跟踪。
¥This will react as expected. All array functions that do not mutate the array are tracked automatically.
autorun(() => {
console.log(message.likes.join(", "))
})
message.likes[2] = "Jennifer"
这将按预期做出反应。检测所有数组索引分配,但仅当 index <= length
。
¥This will react as expected. All array index assignments are detected, but only if index <= length
.
错误的:"use" 是可观察的,但不访问其任何属性
¥Incorrect: "use" an observable but without accessing any of its properties
autorun(() => {
message.likes
})
message.likes.push("Jennifer")
这样就不会反应了。原因很简单,因为 likes
数组本身并未被 autorun
使用,仅使用对该数组的引用。因此相比之下,message.likes = ["Jennifer"]
将被拾取;该语句不会修改数组,而是修改 likes
属性本身。
¥This will not react. Simply because the likes
array itself is not being used by the autorun
, only the reference to the array.
So in contrast, message.likes = ["Jennifer"]
would be picked up; that statement does not modify the array, but the likes
property itself.
正确的:使用尚不存在的映射条目
¥Correct: using not yet existing map entries
const twitterUrls = observable.map({
Joe: "twitter.com/joey"
})
autorun(() => {
console.log(twitterUrls.get("Sara"))
})
runInAction(() => {
twitterUrls.set("Sara", "twitter.com/horsejs")
})
这样就会有反应了。可观察映射支持观察可能不存在的条目。请注意,这最初将打印 undefined
。你可以首先使用 twitterUrls.has("Sara")
检查条目是否存在。因此,在没有代理支持动态键控集合的环境中,始终使用可观察映射。如果你确实有代理支持,你也可以使用可观察的映射,但你也可以选择使用普通对象。
¥This will react. Observable maps support observing entries that may not exist.
Note that this will initially print undefined
.
You can check for the existence of an entry first by using twitterUrls.has("Sara")
.
So in an environment without Proxy support for dynamically keyed collections always use observable maps. If you do have Proxy support you can use observable maps as well,
but you also have the option to use plain objects.
MobX 不跟踪异步访问的数据
¥MobX does not track asynchronously accessed data
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
中发生的解除引用,因为它发生在自动运行执行期间。
¥This will react. Even though author.name
is not dereferenced by the function passed to autorun
itself, MobX will still track the dereferencing that happens in upperCaseAuthorName
, because it happens during the execution of the autorun.
autorun(() => {
setTimeout(() => console.log(message.likes.join(", ")), 10)
})
runInAction(() => {
message.likes.push("Jennifer")
})
这不会做出反应,因为在执行 autorun
期间没有访问任何可观察对象,仅在 setTimeout
期间访问,这是一个异步函数。
¥This will not react because during the execution of the autorun
no observables were accessed, only during the setTimeout
, which is an asynchronous function.
另请查看 异步操作 部分。
¥Check out the Asynchronous actions section as well.
使用不可观察的对象属性
¥Using non-observable object properties
autorun(() => {
console.log(message.author.age)
})
runInAction(() => {
message.author.age = 10
})
如果你在支持 Proxy 的环境中运行 React,这将会做出反应。请注意,这仅适用于使用 observable
或 observable.object
创建的对象。类实例上的新属性不会自动变得可观察。
¥This will react if you run React in an environment that supports Proxy.
Note that this is only done for objects created with observable
or observable.object
. New properties on class instances will not be made observable automatically.
没有代理支持的环境
¥Environments without Proxy support
这样就不会反应了。MobX 只能跟踪 observable 属性,而 'age' 还没有被定义为上面的 observable 属性。
¥This will not react. MobX can only track observable properties, and 'age' has not been defined as observable property above.
但是,可以使用 MobX 公开的 get
和 set
方法来解决此问题:
¥However, it is possible to use the get
and set
methods as exposed by MobX to work around this:
import { get, set } from "mobx"
autorun(() => {
console.log(get(message.author, "age"))
})
set(message.author, "age", 10)
[无代理支持] 错误:使用尚不存在的可观察对象属性
¥[Without Proxy support] Incorrect: using not yet existing observable object properties
autorun(() => {
console.log(message.author.age)
})
extendObservable(message.author, {
age: 10
})
这样就不会反应了。MobX 不会对跟踪开始时不存在的可观察属性做出反应。如果两个语句交换,或者任何其他可观察导致 autorun
重新运行,则 autorun
也将开始跟踪 age
。
¥This will not react. MobX will not react to observable properties that did not exist when tracking started.
If the two statements are swapped, or if any other observable causes the autorun
to re-run, the autorun
will start tracking the age
as well.
[无代理支持] 正确:使用 MobX 工具读取/写入对象
¥[Without Proxy support] Correct: using MobX utilities to read / write to objects
如果你处于没有代理支持的环境中,并且仍然希望将可观察对象用作动态集合,则可以使用 MobX get
和 set
API 来处理它们。
¥If you are in an environment without proxy support and still want to use observable
objects as a dynamic collection, you can handle them using the MobX get
and set
API.
以下也会做出反应:
¥The following will react as well:
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 了解更多详细信息。
¥Check out the Collection utilities API for more details.
TL;DR
MobX 对在跟踪函数执行期间读取的任何现有可观察属性做出反应。
¥MobX reacts to any existing observable property that is read during the execution of a tracked function.