了解反應性
MobX 通常會如您預期地反應,這表示在 90% 的使用案例中,MobX 應該可以「正常運作」。然而,有時您會遇到 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: 'Message@1.title'
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: 'Message@1.title' } ] }
錯誤:更改不可觀察的參考
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` 中使用。
如果您在網頁瀏覽器的除錯工具中使用它,您可能仍然可以找到 `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
的情況下。
錯誤:「使用」一個可觀察物件,但沒有訪問它的任何屬性
autorun(() => {
message.likes
})
message.likes.push("Jennifer")
這將**不會**反應。僅僅是因為 likes
陣列本身沒有被傳遞給 autorun
的函式使用,只有陣列的參考被使用了。相反地,message.likes = ["Jennifer"]
將會被捕捉到;該語句並未修改陣列,而是修改了 likes
屬性本身。
正確:使用尚未存在的映射項目
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")
檢查項目是否存在。因此,在不支援 Proxy 以動態鍵控集合的環境中,請始終使用可觀察映射。如果您確實有 Proxy 支援,您也可以使用可觀察映射,但您也可以選擇使用普通物件。
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
的執行期間沒有訪問任何可觀察物件,只有在 setTimeout
期間訪問了,而這是一個非同步函式。
也請查看非同步動作章節。
使用不可觀察的物件屬性
autorun(() => {
console.log(message.author.age)
})
runInAction(() => {
message.author.age = 10
})
如果您在支援 Proxy 的環境中運行 React,這將**會**反應。請注意,這僅適用於使用 observable
或 observable.object
建立的物件。類別實例上的新屬性不會自動變成可觀察的。
不支援 Proxy 的環境
這將**不會**反應。MobX 只能追蹤可觀察的屬性,而上述的 'age' 並未定義為可觀察的屬性。
然而,可以使用 MobX 公開的 get
和 set
方法來解決這個問題
import { get, set } from "mobx"
autorun(() => {
console.log(get(message.author, "age"))
})
set(message.author, "age", 10)
[不支援 Proxy 的情況下] 錯誤:使用尚未存在的可觀察物件屬性
autorun(() => {
console.log(message.author.age)
})
extendObservable(message.author, {
age: 10
})
這將**不會**反應。MobX 不會對追蹤開始時不存在的可觀察屬性做出反應。如果將這兩個語句交換,或者任何其他可觀察物件導致 autorun
重新運行,則 autorun
也將開始追蹤 age
。
[不支援 Proxy 的情況下] 正確:使用 MobX 工具來讀取/寫入物件
如果您在不支援 proxy 的環境中,並且仍然想使用可觀察物件作為動態集合,您可以使用 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了解更多詳細資訊。
總結
MobX 會對在追蹤函式執行期間讀取的任何現有 可觀察 屬性做出反應。