使用 reactions 執行副作用 {🚀}
Reactions 是一個重要的概念,因為它是 MobX 所有功能的匯集點。Reactions 的目標是將自動發生的副作用建模。它們的重要性在於為您的可觀察狀態建立消費者,並在任何*相關*內容發生變化時*自動*執行副作用。
然而,請務必理解,這裡討論的 API 應該很少直接使用。它們通常會被抽象化到其他函式庫(例如 mobx-react)或您的應用程式特定的抽象層中。
但是,為了理解 MobX,讓我們來看看如何建立 reactions。最簡單的方法是使用 autorun
工具。除此之外,還有 reaction
和 when
。
Autorun (自動執行)
用法
autorun(effect: (reaction) => void, options?)
autorun
函式接受一個函式,該函式應在其觀察的任何內容發生變化時執行。當您建立 autorun
本身時,它也會執行一次。它只會對可觀察狀態的變化做出反應,也就是您使用 observable
或 computed
標註的內容。
追蹤如何運作
Autorun 的運作方式是在一個*反應性上下文*中執行 effect
。在提供的函式執行期間,MobX 會追蹤所有被 effect 直接或間接*讀取*的可觀察值和計算值。一旦函式完成,MobX 將收集並訂閱所有被讀取的可觀察物件,並等待它們再次發生變化。一旦發生變化,autorun
將再次觸發,重複整個過程。
這就是以下範例的運作方式。
import { makeAutoObservable, autorun } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
autorun(() => {
console.log("Energy level:", giraffe.energyLevel)
})
autorun(() => {
if (giraffe.isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
})
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
執行此程式碼,您將獲得以下輸出
Energy level: 100
I'm not hungry!
Now let's change state!
Energy level: 90
Energy level: 80
Energy level: 70
Energy level: 60
Energy level: 50
Energy level: 40
Now I'm hungry!
Energy level: 30
Energy level: 20
Energy level: 10
Energy level: 0
如您在上方輸出的前兩行中所見,兩個 autorun
函式在初始化時都會執行一次。如果沒有 for
迴圈,這就是您將看到的全部內容。
一旦我們執行 for
迴圈使用 reduceEnergy
動作來更改 energyLevel
,我們會在每次 autorun
函式觀察到其可觀察狀態發生變化時看到一個新的日誌條目
對於*“能量等級”*函式,這是每次
energyLevel
可觀察物件發生變化時,總共 10 次。對於*“現在我餓了”*函式,這是每次
isHungry
計算值發生變化時,只有一次。
Reaction (反應)
用法
reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
.
reaction
類似於 autorun
,但可以更精細地控制要追蹤哪些可觀察物件。它需要兩個函式:第一個是*資料*函式,它會被追蹤並返回用作第二個函式輸入的資料,即*效果*函式。需要注意的是,副作用*僅*對在資料函式中*被存取*的資料做出反應,這可能少於實際上在效果函式中使用的資料。
典型的模式是您在*資料*函式中產生副作用所需的內容,並以此方式更精確地控制效果觸發的時機。預設情況下,*資料*函式的結果必須更改才能觸發*效果*函式。與 autorun
不同的是,副作用在初始化時不會執行一次,而是在資料表達式第一次返回新值之後才會執行。
**範例:**資料和效果函式
在下面的範例中,reaction 只會觸發一次,即當 isHungry
更改時。對 giraffe.energyLevel
的更改(由*效果*函式使用)不會導致*效果*函式被執行。如果您希望 reaction
也對此做出反應,則必須也在*資料*函式中存取它並將其返回。
import { makeAutoObservable, reaction } from "mobx"
class Animal {
name
energyLevel
constructor(name) {
this.name = name
this.energyLevel = 100
makeAutoObservable(this)
}
reduceEnergy() {
this.energyLevel -= 10
}
get isHungry() {
return this.energyLevel < 50
}
}
const giraffe = new Animal("Gary")
reaction(
() => giraffe.isHungry,
isHungry => {
if (isHungry) {
console.log("Now I'm hungry!")
} else {
console.log("I'm not hungry!")
}
console.log("Energy level:", giraffe.energyLevel)
}
)
console.log("Now let's change state!")
for (let i = 0; i < 10; i++) {
giraffe.reduceEnergy()
}
輸出
Now let's change state!
Now I'm hungry!
Energy level: 40
When (當)
用法
when(predicate: () => boolean, effect?: () => void, options?)
when(predicate: () => boolean, options?): Promise
when
觀察並執行給定的*謂詞*函式,直到它返回 true
。一旦發生這種情況,就會執行給定的*效果*函式,並且 autorunner 將被釋放。
when
函式會返回一個 disposer,允許您手動取消它,除非您沒有傳入第二個 effect
函式,在這種情況下它會返回一個 Promise
。
**範例:**以反應方式處置事物
when
非常適用於以反應方式處置或取消事物。例如
import { when, makeAutoObservable } from "mobx"
class MyResource {
constructor() {
makeAutoObservable(this, { dispose: false })
when(
// Once...
() => !this.isVisible,
// ... then.
() => this.dispose()
)
}
get isVisible() {
// Indicate whether this item is visible.
}
dispose() {
// Clean up some resources.
}
}
一旦 isVisible
變為 false
,就會呼叫 dispose
方法,然後對 MyResource
進行一些清理。
await when(...)
如果沒有提供 effect
函式,when
會返回一個 Promise
。這與 async / await
良好地結合,讓您可以等待可觀察狀態的變化。
async function() {
await when(() => that.isVisible)
// etc...
}
要提前取消`when`,可以呼叫它返回的 promise 上的`.cancel()`方法。
規則
有一些規則適用於任何反應性上下文
- 如果一個可觀察物件發生變化,受影響的反應預設會立即(同步)執行。但是,它們不會在當前最外層(事務)結束之前運行。
- Autorun 只會追蹤在所提供函數同步執行期間讀取的可觀察物件,但它不會追蹤任何非同步發生的事件。
- Autorun 不會追蹤由 autorun 調用的 action 所讀取的可觀察物件,因為 action 總是*未被追蹤的*。
關於 MobX 精確會和不會對哪些事件做出反應的更多示例,請查看理解反應性章節。有關追蹤工作原理的更詳細技術說明,請閱讀部落格文章變得完全反應性:MobX 的深入解釋。
永遠都要釋放反應
傳遞給 `autorun`、`reaction` 和 `when` 的函數只有在它們觀察的所有物件都被垃圾回收時才會被垃圾回收。原則上,它們會一直等待它們使用的可觀察物件發生新的變化。為了能夠阻止它們永遠等待下去,它們都會返回一個 disposer 函數,可以用來停止它們並取消訂閱它們使用的任何可觀察物件。
const counter = observable({ count: 0 })
// Sets up the autorun and prints 0.
const disposer = autorun(() => {
console.log(counter.count)
})
// Prints: 1
counter.count++
// Stops the autorun.
disposer()
// Will not print.
counter.count++
我們強烈建議一旦不再需要它們的副作用,就永遠使用從這些方法返回的 disposer 函數。否則可能會導致記憶體洩漏。
作為第二個參數傳遞給 `reaction` 和 `autorun` 的 effect 函數的 `reaction` 參數,也可以通過調用 `reaction.dispose()` 來提前清除反應。
**範例:** 記憶體洩漏
class Vat {
value = 1.2
constructor() {
makeAutoObservable(this)
}
}
const vat = new Vat()
class OrderLine {
price = 10
amount = 1
constructor() {
makeAutoObservable(this)
// This autorun will be GC-ed together with the current orderline
// instance as it only uses observables from `this`. It's not strictly
// necessary to dispose of it once an OrderLine instance is deleted.
this.disposer1 = autorun(() => {
doSomethingWith(this.price * this.amount)
})
// This autorun won't be GC-ed together with the current orderline
// instance, since vat keeps a reference to notify this autorun, which
// in turn keeps 'this' in scope.
this.disposer2 = autorun(() => {
doSomethingWith(this.price * this.amount * vat.value)
})
}
dispose() {
// So, to avoid subtle memory issues, always call the
// disposers when the reactions are no longer needed.
this.disposer1()
this.disposer2()
}
}
謹慎使用反應!
如前所述,您不會經常創建反應。您的應用程式很可能根本沒有直接使用任何這些 API,而構建反應的唯一途徑是間接的,例如通過 mobx-react 綁定中的 `observer`。
在設置反應之前,最好先檢查它是否符合以下原則
- **僅在因果關係沒有直接關聯時才使用反應**:如果副作用應該響應一組非常有限的事件/動作,則直接從這些特定動作觸發副作用通常會更清晰。例如,如果按下表單提交按鈕應該導致發布網路請求,則直接響應 `onClick` 事件來觸發此效果會更清晰,而不是通過反應間接觸發。相反,如果您對表單狀態所做的任何更改都應自動儲存在本地儲存空間中,則反應會非常有用,這樣您就不必從每個單獨的 `onChange` 事件觸發此效果。
- **反應不應更新其他可觀察物件**:反應是否會修改其他可觀察物件?如果答案是肯定的,則通常應將要更新的可觀察物件註釋為`computed`值。例如,如果更改了待辦事項集合,請不要使用反應來計算 `remainingTodos` 的數量,而是將 `remainingTodos` 註釋為計算值。這將使程式碼更清晰、更容易除錯。反應不應計算新數據,而應僅產生效果。
- **反應應該是獨立的**:您的程式碼是否依賴於其他一些必須先執行的反應?如果是這種情況,您可能違反了第一條規則,或者您即將創建的新反應應該與它所依賴的反應合併。MobX 不保證反應的執行順序。
現實生活中有些情況不符合上述原則。這就是為什麼它們是*原則*,而不是*定律*。但是,例外情況很少見,因此只有在不得已的情況下才違反它們。
選項 {🚀}
可以通過傳入 `options` 參數來進一步微調 `autorun`、`reaction` 和 `when` 的行為,如上例所示。
`name`
此字串在Spy 事件監聽器和MobX 開發者工具中用作此反應的除錯名稱。
`fireImmediately` *(reaction)*
布林值,指示在第一次運行 *data* 函數後應立即觸發 *effect* 函數。預設為 `false`。
`delay` *(autorun, reaction)*
可以用於限制 effect 函數的毫秒數。如果為零(預設),則不會發生限制。
`timeout` *(when)*
設定 `when` 將等待的有限時間。如果超過期限,`when` 將拒絕/拋出錯誤。
signal
一個 AbortSignal 物件實例;可以用作釋放的替代方法。
當與 `when` 的 promise 版本一起使用時,promise 會以 "WHEN_ABORTED" 錯誤拒絕。
`onError`
預設情況下,反應內部拋出的任何異常都將被記錄,但不會進一步拋出。這是為了確保一個反應中的異常不會阻止其他可能不相關的反應的排程執行。這也允許反應從異常中恢復。拋出異常不會破壞 MobX 進行的追蹤,因此如果異常的原因被移除,則反應的後續運行可能會再次正常完成。此選項允許覆蓋該行為。可以使用配置設置全域錯誤處理程式或完全禁用錯誤捕獲。
scheduler
(自動執行,反應)
設定一個自定義排程器,以決定如何排程重新執行自動執行函式。它接受一個應該在未來某個時間點被呼叫的函式,例如:{ scheduler: run => { setTimeout(run, 1000) }}
equals
:(反應)
預設設定為 comparer.default
。如果指定,此比較器函式將用於比較由 *data* 函式產生的先前值和下一個值。僅當此函式返回 false 時,才會呼叫 *effect* 函式。
查看內建比較器章節。