使用反应执行副作用 {🚀}
反应是一个重要的概念,它是 MobX 中所有东西汇聚的地方。反应的目标是模拟自动发生的副作用。它们的意义在于为您的可观察状态创建消费者,并在任何相关内容发生变化时自动执行副作用。
但是,请注意,这里讨论的 API 很少使用。它们通常在其他库(如 mobx-react)或特定于您的应用程序的抽象中被抽象化。
但是,为了理解 MobX,让我们看看如何创建反应。最简单的方法是使用 autorun
实用程序。除此之外,还有 reaction
和 when
。
Autorun
用法
autorun(effect: (reaction) => void, options?)
autorun
函数接受一个函数,该函数应该在它观察到的任何东西发生变化时运行。它在您创建 autorun
本身时也会运行一次。它只对可观察状态中的变化做出响应,即您已注释为 observable
或 computed
的内容。
跟踪工作原理
Autorun 通过在响应式上下文中运行 effect
来工作。在执行提供的函数期间,MobX 会跟踪直接或间接由效果读取的所有可观察和计算值。函数完成后,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(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
.
reaction
类似于 autorun
,但对要跟踪哪些可观察值提供了更细粒度的控制。它接受两个函数:第一个数据函数被跟踪并返回用作第二个效果函数输入的数据。重要的是要注意,副作用只对数据函数中访问的数据做出反应,这可能少于效果函数中实际使用的数据。
典型的模式是您在数据函数中生成副作用中需要的东西,并以此方式更精确地控制副作用触发的时机。默认情况下,数据函数的结果必须改变,才能触发效果函数。与 autorun
不同,副作用不会在初始化时运行一次,而是在数据表达式第一次返回新值后才运行。
示例:数据和效果函数
在下面的示例中,反应只触发一次,即 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(predicate: () => boolean, effect?: () => void, options?)
when(predicate: () => boolean, options?): Promise
when
观察并运行给定的谓词函数,直到它返回 true
。一旦发生这种情况,给定的效果函数将被执行,并且 autorunner 将被处置。
when
函数返回一个处置器,允许您手动取消它,除非您没有传入第二个 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()
。
规则
有一些规则适用于任何响应式上下文
- 如果可观察值发生变化,默认情况下,受影响的反应会立即(同步)运行。但是,它们不会在当前最外层(trans)操作结束之前运行。
- Autorun 仅跟踪在提供的函数的同步执行期间读取的可观察值,但它不会跟踪异步发生的任何事情。
- Autorun 不会跟踪由 autorun 调用的操作读取的可观察值,因为操作始终是未跟踪的。
有关 MobX 究竟会对什么做出反应以及不会对什么做出反应的更多示例,请查看 理解响应性 部分。有关跟踪工作原理的更详细的技术分解,请阅读博客文章 Becoming fully reactive: an in-depth explanation of MobX。
始终处置反应
传递给 autorun
、reaction
和 when
的函数只有在它们观察的所有对象都被垃圾回收后才会被垃圾回收。原则上,它们会一直等待新变化发生,直到永远过去。为了能够阻止它们等待,直到永远过去,它们都返回一个处置器函数,该函数可用于停止它们并取消订阅它们使用的任何可观察值。
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++
强烈建议始终使用从这些方法返回的 `dispose` 函数,只要它们的副作用不再需要。 否则会导致内存泄漏。
传递给 `reaction` 和 `autorun` 的效果函数的第二个参数的 `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)
布尔值,指示在第一次运行数据函数后是否应立即触发效果函数。 默认情况下为 `false`。
delay
(autorun, reaction)
可用于限制效果函数的毫秒数。 如果为零(默认),则不进行限制。
timeout
(when)
设置 `when` 将等待的有限时间。 如果截止日期过去,`when` 将拒绝/抛出。
signal
AbortSignal 对象实例; 可用作处置的替代方法。
当与 `when` 的 promise 版本一起使用时,promise 会因“WHEN_ABORTED”错误而拒绝。
onError
默认情况下,反应内部抛出的任何异常都会被记录,但不会被进一步抛出。 这是为了确保一个反应中的异常不会阻止其他可能无关反应的计划执行。 这也允许反应从异常中恢复。 抛出异常不会破坏 MobX 执行的跟踪,因此如果消除了导致异常的原因,反应的后续运行可能会再次正常完成。 此选项允许覆盖该行为。 可以使用 configure 设置全局错误处理程序或完全禁用捕获错误。
scheduler
(autorun, reaction)
设置自定义调度程序以确定如何安排重新运行 autorun 函数。 它接受一个将在将来某个时间点调用的函数,例如:{ scheduler: run => { setTimeout(run, 1000) }}
equals
: (reaction)
默认情况下设置为 `comparer.default`。 如果指定,此比较器函数用于比较数据函数产生的上一个和下一个值。 仅当此函数返回 `false` 时才会调用效果函数。
查看内置比较器 部分。