运行副作用与反应 {🚀}
¥Running side effects with reactions {🚀}
反应是一个需要理解的重要概念,因为它是 MobX 中所有内容的结合点。反应的目标是对自动发生的副作用进行建模。它们的重要性在于为你的可观察状态创建消费者,并在相关变化时自动运行副作用。
¥Reactions are an important concept to understand, as it is where everything in MobX comes together. The goal of reactions is to model side effects that happen automatically. Their significance is in creating consumers for your observable state and automatically running side effects whenever something relevant changes.
然而,考虑到这一点,重要的是要认识到这里讨论的 API 应该很少使用。它们通常在其他库(如 mobx-react)或特定于你的应用的抽象中被抽象出来。
¥However, with that in mind, it is important to realize that the APIs discussed here should rarely be used. They are often abstracted away in other libraries (like mobx-react) or abstractions specific to your application.
但是,为了理解 MobX,让我们看一下如何创建反应。最简单的方法是使用 autorun
实用程序。除此之外,还有 reaction
和 when
。
¥But, to grok MobX, let's take a look at how reactions can be created.
The simplest way is to use the autorun
utility.
Beyond that, there are also reaction
and when
.
自动运行
¥Autorun
用法:
¥Usage:
autorun(effect: (reaction) => void, options?)
autorun
函数接受一个函数,该函数应在每次观察到任何变化时运行。当你创建 autorun
本身时,它也会运行一次。它仅响应可观察状态的变化,即你注释为 observable
或 computed
的内容。
¥The autorun
function accepts one function that should run every time anything it observes changes.
It also runs once when you create the autorun
itself. It only responds to changes in observable state, things you have annotated observable
or computed
.
跟踪的工作原理
¥How tracking works
自动运行的工作原理是在响应式上下文中运行 effect
。在执行所提供的函数期间,MobX 会跟踪效果直接或间接读取的所有可观察和计算值。一旦函数完成,MobX 将收集并订阅所有已读取的可观察量,并等待其中任何一个再次发生变化。一旦发生,autorun
将再次触发,重复整个过程。
¥Autorun works by running the effect
in a reactive context. During the execution of the provided function, MobX keeps track of all observable and computed values that are directly or indirectly read by the effect.
Once the function finishes, MobX will collect and subscribe to all observables that were read and wait until any of them changes again.
Once they do, the autorun
will trigger again, repeating the entire process.
这就是下面的示例的工作原理。
¥This is how the example below works like.
示例
¥Example
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()
}
运行此代码,你将得到以下输出:
¥Running this code, you will get the following output:
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
循环的情况下你所看到的全部内容。
¥As you can see in the first two lines of the output above, both autorun
functions run once when they are initialized. This is all you would see without the for
loop.
一旦我们运行 for
循环以通过 reduceEnergy
操作更改 energyLevel
,每次 autorun
函数观察到其可观察状态发生变化时,我们都会看到一个新的日志条目:
¥Once we run the for
loop to change the energyLevel
with the reduceEnergy
action, we see a new log entry every time an autorun
function observes a
change in its observable state:
对于 "能级" 函数,这是
energyLevel
可观察到的每次变化,总共 10 次。¥For the "Energy level" function, this is every time the
energyLevel
observable changes, 10 times in total.对于 "现在我饿了" 函数,这是每次
isHungry
计算发生变化时,仅一次。¥For the "Now I'm hungry" function, this is every time the
isHungry
computed changes, only one time.
反应
¥Reaction
用法:
¥Usage:
reaction(() => value, (value, previousValue, reaction) => { sideEffect }, options?)
。
reaction
与 autorun
类似,但对跟踪哪些可观察量提供了更细粒度的控制。它需要两个函数:第一个数据函数被跟踪并返回用作第二个效果函数的输入的数据。值得注意的是,副作用仅对数据函数中访问的数据做出反应,该数据可能小于效果函数中实际使用的数据。
¥reaction
is like autorun
, but gives more fine grained control on which observables will be tracked.
It takes two functions: the first, data function, is tracked and returns the data that is used as input for the second, effect function.
It is important to note that the side effect only reacts to data that was accessed in the data function, which might be less than the data that is actually used in the effect function.
典型的模式是,你在数据函数中产生副作用所需的东西,并以这种方式更精确地控制效果何时触发。默认情况下,数据函数的结果必须更改才能触发效果函数。与 autorun
不同,副作用在初始化时不会运行一次,而是仅在数据表达式第一次返回新值后运行。
¥The typical pattern is that you produce the things you need in your side effect
in the data function, and in that way control more precisely when the effect triggers.
By default, the result of the data function has to change in order for the effect function to be triggered.
Unlike autorun
, the side effect won't run once when initialized, but only after the data expression returns a new value for the first time.
Example: the data and effect functions
在下面的示例中,反应仅在 isHungry
更改时触发一次。对效果函数使用的 giraffe.energyLevel
进行更改不会导致执行效果函数。如果你希望 reaction
也对此做出响应,则还必须在数据函数中访问它并返回它。
¥In the example below, the reaction is only triggered once, when isHungry
changes.
Changes to giraffe.energyLevel
, which is used by the effect function, do not cause the effect function to be executed. If you wanted reaction
to respond to this
as well, you would have to also access it in the data function and return it.
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()
}
输出:
¥Output:
Now let's change state!
Now I'm hungry!
Energy level: 40
何时
¥When
用法:
¥Usage:
when(predicate: () => boolean, effect?: () => void, options?)
when(predicate: () => boolean, options?): Promise
when
观察并运行给定的谓词函数,直到它返回 true
。一旦发生这种情况,就会执行给定的效果函数并处理自动运行器。
¥when
observes and runs the given predicate function until it returns true
.
Once that happens, the given effect function is executed and the autorunner is disposed.
when
函数返回一个处理程序,允许你手动取消它,除非你不传入第二个 effect
函数,在这种情况下它会返回 Promise
。
¥The when
function returns a disposer, allowing you to cancel it manually, unless you don't pass in a second effect
function, in which case it returns a Promise
.
Example: dispose of things in a reactive way
when
对于以反应方式处理或取消事物非常有用。例如:
¥when
is really useful for disposing or canceling of things in a reactive way.
For example:
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
进行一些清理。
¥As soon as isVisible
becomes false
, the dispose
method is called that
then does some cleanup for MyResource
.
await when(...)
如果未提供 effect
函数,则 when
返回 Promise
。这与 async / await
很好地结合在一起,让你等待可观察状态的变化。
¥If no effect
function is provided, when
returns a Promise
. This combines nicely with async / await
to let you wait for changes in observable state.
async function() {
await when(() => that.isVisible)
// etc...
}
要提前取消 when
,可以在其返回的 Promise 上调用 .cancel()
。
¥To cancel when
prematurely, it is possible to call .cancel()
on the promise returned by itself.
规则
¥Rules
有一些规则适用于任何反应式上下文:
¥There are a few rules that apply to any reactive context:
如果可观察值发生更改,受影响的反应默认立即(同步)运行。但是,它们不会在当前最外层(事务)操作结束之前运行。
¥Affected reactions run by default immediately (synchronously) if an observable is changed. However, they won't run before the end of the current outermost (trans)action.
Autorun 仅跟踪在提供的函数的同步执行期间读取的可观察量,但它不会跟踪异步发生的任何内容。
¥Autorun tracks only the observables that are read during the synchronous execution of the provided function, but it won't track anything that happens asynchronously.
自动运行不会跟踪由自动运行调用的操作读取的可观察量,因为操作始终未被跟踪。
¥Autorun won't track observables that are read by an action invoked by the autorun, as actions are always untracked.
有关 MobX 会做出反应和不会做出反应的更多示例,请查看 了解反应性 部分。有关跟踪工作原理的更详细技术分析,请阅读博客文章 变得完全被动:MobX 的深入解释。
¥For more examples on what precisely MobX will and will not react to, check out the Understanding reactivity section. For a more detailed technical breakdown on how tracking works, read the blog post Becoming fully reactive: an in-depth explanation of MobX.
始终处理反应
¥Always dispose of reactions
仅当传递给 autorun
、reaction
和 when
的所有对象本身都被垃圾回收时,它们才会被垃圾回收。原则上,他们会永远等待他们使用的可观察量发生新的变化。为了能够阻止它们等待永远过去,它们都返回一个处理函数,可用于停止它们并取消订阅它们使用的任何可观察量。
¥The functions passed to autorun
, reaction
and when
are only garbage collected if all objects they observe are garbage collected themselves. In principle, they keep waiting forever for new changes to happen in the observables they use.
To be able to stop them from waiting until forever has passed, they all return a disposer function that can be used to stop them and unsubscribe from any observables they used.
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++
我们强烈建议一旦不再需要这些方法的副作用,就始终使用从这些方法返回的处理程序函数。如果不这样做可能会导致内存泄漏。
¥We strongly recommend to always use the disposer function that is returned from these methods as soon as their side effect is no longer needed. Failing to do so can lead to memory leaks.
作为第二个参数传递给 reaction
和 autorun
效果函数的 reaction
参数也可用于通过调用 reaction.dispose()
来提前清理反应。
¥The reaction
argument that is passed as second argument to the effect functions of reaction
and autorun
, can be used to prematurely clean up the reaction as well by calling reaction.dispose()
.
Example: memory leak
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()
}
}
谨慎使用反应!
¥Use reactions sparingly!
正如已经说过的,你不会经常产生反应。你的应用很可能不直接使用任何这些 API,而构建反应的唯一方式是间接的,例如通过 mobx-react 绑定中的 observer
。
¥As it was already said, you won't create reactions very often.
It might very well be that your application doesn't use any of these APIs directly, and the only way reactions are constructed is indirectly, through for example observer
from the mobx-react bindings.
在设置反应之前,最好先检查它是否符合以下原则:
¥Before you set up a reaction, it is good to first check if it conforms to the following principles:
仅当因果关系没有直接关系时才使用反应:如果副作用是响应一组非常有限的事件/操作而发生的,那么直接从这些特定操作触发效果通常会更清楚。例如,如果按下表单提交按钮会导致发布网络请求,那么直接响应
onClick
事件来触发此效果比通过反应间接触发更清楚。相反,如果你对表单状态所做的任何更改都应自动最终存储在本地存储中,那么反应可能会非常有用,这样你就不必从每个单独的onChange
事件中触发此效果。¥Only use Reactions if there is no direct relation between cause and effect: If a side effect should happen in response to a very limited set of events / actions, it will often be clearer to directly trigger the effect from those specific actions. For example, if pressing a form submit button should lead to a network request to be posted, it is clearer to trigger this effect directly in response of the
onClick
event, rather than indirectly through a reaction. In contrast, if any change you make to the form state should automatically end up in local storage, then a reaction can be very useful, so that you don't have to trigger this effect from every individualonChange
event.反应不应更新其他可观察量:该反应会改变其他可观测值吗?如果答案是肯定的,通常你想要更新的可观察量应该被注释为
computed
值。例如,如果更改了待办事项集合,则不要使用反应来计算remainingTodos
的数量,而是将remainingTodos
注释为计算值。这将导致更清晰、更容易调试的代码。反应不应计算新数据,而应仅产生影响。¥Reactions shouldn't update other observables: Is the reaction going to modify other observables? If the answer is yes, typically the observable you want to update should be annotated as a
computed
value instead. For example, if a collection of todos is altered, don't use a reaction to compute the amount ofremainingTodos
, but annotateremainingTodos
as a computed value. That will lead to much clearer and easier to debug code. Reactions should not compute new data, but only cause effects.反应应该是独立的:你的代码是否依赖于必须首先运行的其他反应?如果是这种情况,你可能违反了第一条规则,或者你将要创建的新反应应该合并到它所依赖的反应中。MobX 不保证反应运行的顺序。
¥Reactions should be independent: Does your code rely on some other reaction having to run first? If that is the case, you probably either violated the first rule, or the new reaction you are about to create should be merged into the one it is depending upon. MobX does not guarantee the order in which reactions will be run.
有些现实生活场景不符合上述原则。这就是为什么它们是原则,而不是法律。但是,例外情况很少见,因此只有在万不得已时才违反它们。
¥There are real-life scenarios that do not fit in the above principles. That is why they are principles, not laws. But, the exceptions are rare so only violate them as a last resort.
选项 {🚀}
¥Options {🚀}
可以通过传入 options
参数来进一步微调 autorun
、reaction
和 when
的行为,如上面的用法所示。
¥The behavior of autorun
, reaction
and when
can be further fine-tuned by passing in an options
argument as shown in the usages above.
name
该字符串用作 间谍事件监听器 和 MobX 开发者工具 中此反应的调试名称。
¥This string is used as a debug name for this reaction in the Spy event listeners and MobX developer tools.
fireImmediately
(反应式)
¥fireImmediately
(reaction)
布尔值,表示在第一次运行数据函数后应立即触发效果函数。默认为 false
。
¥Boolean indicating that the effect function should immediately be triggered after the first run of the data function. false
by default.
delay
(自动运行、反应式)
¥delay
(autorun, reaction)
可用于限制效果函数的毫秒数。如果为零(默认),则不会发生限制。
¥Number of milliseconds that can be used to throttle the effect function. If zero (default), no throttling happens.
timeout
(何时)
¥timeout
(when)
设置 when
等待的有限时间。如果截止日期已过,when
将拒绝/抛出。
¥Set a limited amount of time that when
will wait for. If the deadline passes, when
will reject / throw.
signal
一个 AbortSignal 对象实例;可以用作处置的替代方法。
当与 when
的 Promise 版本一起使用时,Promise 会拒绝并出现 "WHEN_ABORTED" 错误。
¥An AbortSignal object instance; can be used as an alternative method for disposal.
When used with promise version of when
, the promise rejects with the "WHEN_ABORTED" error.
onError
默认情况下,反应中引发的任何异常都将被记录,但不会进一步引发。这是为了确保一个反应中的异常不会妨碍其他可能不相关的反应的计划执行。这也允许反应从异常中恢复。抛出异常不会破坏 MobX 完成的跟踪,因此如果异常原因被消除,后续的反应运行可能会再次正常完成。此选项允许覆盖该行为。可以使用 configure 设置全局错误处理程序或完全禁用捕获错误。
¥By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using configure.
scheduler
(自动运行、反应式)
¥scheduler
(autorun, reaction)
设置自定义调度程序以确定如何安排重新运行自动运行功能。它需要一个应该在将来某个时刻调用的函数,例如:{ scheduler: run => { setTimeout(run, 1000) }}
¥Set a custom scheduler to determine how re-running the autorun function should be scheduled. It takes a function that should be invoked at some point in the future, for example: { scheduler: run => { setTimeout(run, 1000) }}
equals
:(反应式)
¥equals
: (reaction)
默认设置为 comparer.default
。如果指定,则此比较器函数用于比较数据函数生成的前一个值和下一个值。仅当该函数返回 false 时才会调用效果函数。
¥Set to comparer.default
by default. If specified, this comparer function is used to compare the previous and next values produced by the data function. The effect function is only invoked if this function returns false.
查看 内置比较器 部分。
¥Check out the Built-in comparers section.