使用操作更新状态
¥Updating state using actions
用法:
¥Usage:
action
(注解)¥
action
(annotation)action(fn)
action(name, fn)
@action
(方法/字段装饰器)¥
@action
(method / field decorator)
所有应用都有操作。操作是修改状态的任何代码段。原则上,动作总是响应事件而发生。例如,单击按钮、更改某些输入、到达 Websocket 消息等。
¥All applications have actions. An action is any piece of code that modifies the state. In principle, actions always happen in response to an event. For example, a button was clicked, some input changed, a websocket message arrived, etc.
MobX 要求你声明你的操作,尽管 makeAutoObservable
可以自动化大部分这项工作。操作可帮助你更好地构建代码并提供以下性能优势:
¥MobX requires that you declare your actions, although makeAutoObservable
can automate much of this job. Actions help you structure your code better and offer the following performance benefits:
它们在 transactions 内运行。在最外层操作完成之前,不会运行任何反应,从而保证操作期间生成的中间值或不完整值在操作完成之前对应用的其余部分不可见。
¥They are run inside transactions. No reactions will be run until the outer-most action has finished, guaranteeing that intermediate or incomplete values produced during an action are not visible to the rest of the application until the action has completed.
默认情况下,不允许在操作之外更改状态。这有助于在代码库中清楚地识别状态更新发生的位置。
¥By default, it is not allowed to change the state outside of actions. This helps to clearly identify in your code base where the state updates happen.
action
注释只能用在打算修改状态的函数上。派生信息(执行查找或过滤数据)的函数不应标记为操作,以允许 MobX 跟踪其调用。action
带注释的成员将是不可枚举的。
¥The action
annotation should only be used on functions that intend to modify the state. Functions that derive information (performing lookups or filtering data) should not be marked as actions, to allow MobX to track their invocations. action
annotated members will be non-enumerable.
示例
¥Examples
import { makeObservable, observable, action } from "mobx"
class Doubler {
value = 0
constructor() {
makeObservable(this, {
value: observable,
increment: action
})
}
increment() {
// Intermediate states will not become visible to observers.
this.value++
this.value++
}
}
import { observable, action } from "mobx"
class Doubler {
@observable accessor value = 0
@action
increment() {
// Intermediate states will not become visible to observers.
this.value++
this.value++
}
}
import { makeAutoObservable } from "mobx"
class Doubler {
value = 0
constructor() {
makeAutoObservable(this)
}
increment() {
this.value++
this.value++
}
}
import { makeObservable, observable, action } from "mobx"
class Doubler {
value = 0
constructor() {
makeObservable(this, {
value: observable,
increment: action.bound
})
}
increment() {
this.value++
this.value++
}
}
const doubler = new Doubler()
// Calling increment this way is safe as it is already bound.
setInterval(doubler.increment, 1000)
import { observable, action } from "mobx"
const state = observable({ value: 0 })
const increment = action(state => {
state.value++
state.value++
})
increment(state)
import { observable, runInAction } from "mobx"
const state = observable({ value: 0 })
runInAction(() => {
state.value++
state.value++
})
action
封装函数
使用 ¥Wrapping functions using action
为了尽可能地利用 MobX 的事务性质,操作应该尽可能向外传递。如果类方法修改了状态,最好将其标记为操作。最好将事件处理程序标记为操作,因为最外层的事务才重要。随后调用两个操作的单个未标记事件处理程序仍会生成两个事务。
¥To leverage the transactional nature of MobX as much as possible, actions should be passed as far outward as possible. It is good to mark a class method as an action if it modifies the state. It is even better to mark event handlers as actions, as it is the outer-most transaction that counts. A single unmarked event handler that calls two actions subsequently would still generate two transactions.
为了帮助创建基于操作的事件处理程序,action
不仅是一个注释,而且还是一个高阶函数。可以使用函数作为参数来调用它,在这种情况下,它将返回具有相同签名的 action
封装函数。
¥To help create action based event handlers, action
is not only an annotation, but also a higher order function. It can be called with a function as an argument, and in that case it will return an action
wrapped function with the same signature.
例如,在 React 中,可以按如下方式封装 onClick
处理程序。
¥For example in React, an onClick
handler can be wrapped as below.
const ResetButton = ({ formState }) => (
<button
onClick={action(e => {
formState.resetPendingUploads()
formState.resetValues()
e.preventDefault()
})}
>
Reset form
</button>
)
出于调试目的,我们建议为封装的函数命名,或者将名称作为第一个参数传递给 action
。
¥For debugging purposes, we recommend to either name the wrapped function, or pass a name as the first argument to action
.
Note: actions are untracked
动作的另一个特点是它们是 untracked。当从副作用或计算值内部调用操作时(非常罕见!),操作读取的可观察量将不计入派生的依赖
¥Another feature of actions is that they are untracked. When an action is called from inside a side effect or a computed value (very rare!), observables read by the action won't be counted towards the dependencies of the derivation
makeAutoObservable
、extendObservable
和 observable
使用 action
的一种特殊风格,称为 autoAction
,它将在运行时确定该函数是派生函数还是动作。
¥makeAutoObservable
, extendObservable
and observable
use a special flavour of action
called autoAction
,
that will determine at runtime if the function is a derivation or action.
action.bound
用法:
¥Usage:
action.bound
(注解)¥
action.bound
(annotation)
action.bound
注释可用于自动将方法绑定到正确的实例,以便 this
始终正确绑定在函数内部。
¥The action.bound
annotation can be used to automatically bind a method to the correct instance, so that this
is always correctly bound inside the function.
Tip: use makeAutoObservable(o, {}, { autoBind: true })
to bind all actions and flows automatically
import { makeAutoObservable } from "mobx"
class Doubler {
value = 0
constructor() {
makeAutoObservable(this, {}, { autoBind: true })
}
increment() {
this.value++
this.value++
}
*flow() {
const response = yield fetch("http://example.com/value")
this.value = yield response.json()
}
}
runInAction
用法:
¥Usage:
runInAction(fn)
使用此实用程序创建立即调用的临时操作。在异步进程中很有用。查看 上面的代码块 的示例。
¥Use this utility to create a temporary action that is immediately invoked. Can be useful in asynchronous processes. Check out the above code block for an example.
动作和继承
¥Actions and inheritance
只有在原型上定义的操作才能被子类覆盖:
¥Only actions defined on prototype can be overridden by subclass:
class Parent {
// on instance
arrowAction = () => {}
// on prototype
action() {}
boundAction() {}
constructor() {
makeObservable(this, {
arrowAction: action
action: action,
boundAction: action.bound,
})
}
}
class Child extends Parent {
// THROWS: TypeError: Cannot redefine property: arrowAction
arrowAction = () => {}
// OK
action() {}
boundAction() {}
constructor() {
super()
makeObservable(this, {
arrowAction: override,
action: override,
boundAction: override,
})
}
}
要将单个操作绑定到 this
,可以使用 action.bound
代替箭头函数。
有关详细信息,请参阅 subclassing。
¥To bind a single action to this
, action.bound
can be used instead of arrow functions.
See subclassing for more information.
异步操作
¥Asynchronous actions
本质上,异步进程在 MobX 中不需要任何特殊处理,因为所有反应都会自动更新,无论它们是在何时引起的。由于可观察对象是可变的,因此在操作期间保留对它们的引用通常是安全的。但是,在异步过程中更新可观察量的每个步骤(勾选)都应标记为 action
。这可以通过利用上述 API 以多种方式实现,如下所示。
¥In essence, asynchronous processes don't need any special treatment in MobX, as all reactions will update automatically regardless of the moment in time they are caused.
And since observable objects are mutable, it is generally safe to keep references to them for the duration of an action.
However, every step (tick) that updates observables in an asynchronous process should be marked as action
.
This can be achieved in multiple ways by leveraging the above APIs, as shown below.
例如,在处理 Promise 时,更新状态的处理程序应该是操作,或者应该使用 action
封装,如下所示。
¥For example, when handling promises, the handlers that update state should be actions or should be wrapped using action
, as shown below.
Promise 解析处理程序是内联处理的,但在原始操作完成后运行,因此它们需要由 action
封装:
¥Promise resolution handlers are handled in-line, but run after the original action finished, so they need to be wrapped by action
:
import { action, makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
action("fetchSuccess", projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}),
action("fetchError", error => {
this.state = "error"
})
)
}
}
如果 Promise 处理程序是类字段,它们将自动被 makeAutoObservable
封装在 action
中:
¥If the promise handlers are class fields, they will automatically be wrapped in action
by makeAutoObservable
:
import { makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(this.projectsFetchSuccess, this.projectsFetchFailure)
}
projectsFetchSuccess = projects => {
const filteredProjects = somePreprocessing(projects)
this.githubProjects = filteredProjects
this.state = "done"
}
projectsFetchFailure = error => {
this.state = "error"
}
}
await
之后的任何步骤都不在同一个刻度中,因此它们需要操作封装。在这里,我们可以利用 runInAction
:
¥Any steps after await
aren't in the same tick, so they require action wrapping.
Here, we can leverage runInAction
:
import { runInAction, makeAutoObservable } from "mobx"
class Store {
githubProjects = []
state = "pending" // "pending", "done" or "error"
constructor() {
makeAutoObservable(this)
}
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
} catch (e) {
runInAction(() => {
this.state = "error"
})
}
}
}
import { flow, makeAutoObservable, flowResult } from "mobx"
class Store {
githubProjects = []
state = "pending"
constructor() {
makeAutoObservable(this, {
fetchProjects: flow
})
}
// Note the star, this a generator function!
*fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
// Yield instead of await.
const projects = yield fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
this.state = "done"
this.githubProjects = filteredProjects
return projects
} catch (error) {
this.state = "error"
}
}
}
const store = new Store()
const projects = await flowResult(store.fetchProjects())
使用 flow 代替 async/await {🚀}
¥Using flow instead of async / await {🚀}
用法:
¥Usage:
flow
(注解)¥
flow
(annotation)flow(function* (args) { })
@flow
(方法装饰器)¥
@flow
(method decorator)
flow
封装器是 async
/ await
的可选替代品,可以更轻松地使用 MobX 操作。flow
将 生成器函数 作为其唯一输入。在生成器内部,你可以通过生成 Promise 来链接它们(而不是编写 await somePromise
,而是编写 yield somePromise
)。然后,流机制将确保生成器在生成的 Promise 解析时继续或抛出。
¥The flow
wrapper is an optional alternative to async
/ await
that makes it easier to
work with MobX actions.
flow
takes a generator function as its only input.
Inside the generator, you can chain promises by yielding them (instead of await somePromise
you write yield somePromise
).
The flow mechanism will then make sure the generator either continues or throws when a yielded promise resolves.
因此 flow
是 async
/ await
的替代品,不需要任何进一步的 action
封装。它可以按如下方式应用:
¥So flow
is an alternative to async
/ await
that doesn't need any further action
wrapping. It can be applied as follows:
将
flow
封装在你的异步函数中。¥Wrap
flow
around your asynchronous function.使用
function *
代替async
。¥Instead of
async
usefunction *
.使用
yield
代替await
。¥Instead of
await
useyield
.
上面的 flow
+ 生成器函数 示例展示了实际情况。
¥The flow
+ generator function example above shows what this looks like in practice.
请注意,仅在使用 TypeScript 时才需要 flowResult
函数。由于用 flow
修饰一个方法,它会将返回的生成器封装在一个 promise 中。但是,TypeScript 不知道该转换,因此 flowResult
将确保 TypeScript 知道该类型更改。
¥Note that the flowResult
function is only needed when using TypeScript.
Since decorating a method with flow
, it will wrap the returned generator in a promise.
However, TypeScript isn't aware of that transformation, so flowResult
will make sure that TypeScript is aware of that type change.
makeAutoObservable
和朋友会自动推断生成器为 flow
。flow
带注释的成员将是不可枚举的。
¥makeAutoObservable
and friends will automatically infer generators to be flow
s. flow
annotated members will be non-enumerable.
{🚀} Note: using flow on object fields
flow
, like action
, can be used to wrap functions directly. The above example could also have been written as follows:
import { flow } from "mobx"
class Store {
githubProjects = []
state = "pending"
fetchProjects = flow(function* (this: Store) {
this.githubProjects = []
this.state = "pending"
try {
// yield instead of await.
const projects = yield fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
this.state = "done"
this.githubProjects = filteredProjects
} catch (error) {
this.state = "error"
}
})
}
const store = new Store()
const projects = await store.fetchProjects()
优点是我们不再需要 flowResult
,缺点是需要键入 this
以确保正确推断其类型。
¥The upside is that we don't need flowResult
anymore, the downside is that this
needs to be typed to make sure its type is inferred correctly.
flow.bound
用法:
¥Usage:
flow.bound
(注解)¥
flow.bound
(annotation)
flow.bound
注释可用于自动将方法绑定到正确的实例,以便 this
始终正确绑定在函数内部。与操作类似,默认情况下可以使用 autoBind
选项 绑定流。
¥The flow.bound
annotation can be used to automatically bind a method to the correct instance, so that this
is always correctly bound inside the function.
Similary to actions, flows can be bound by default using autoBind
option.
取消流 {🚀}
¥Cancelling flows {🚀}
流的另一个好处是它们是可以取消的。flow
的返回值是一个 Promise,它最终使用生成器函数返回的值进行解析。返回的 Promise 有一个额外的 cancel()
方法,该方法将中断正在运行的生成器并取消它。任何 try
/ finally
子句仍将运行。
¥Another neat benefit of flows is that they are cancellable.
The return value of flow
is a promise that resolves with the value that is returned from the generator function in the end.
The returned promise has an additional cancel()
method that will interrupt the running generator and cancel it.
Any try
/ finally
clauses will still be run.
禁用强制操作 {🚀}
¥Disabling mandatory actions {🚀}
默认情况下,MobX 6 及更高版本要求你使用操作来更改状态。但是,你可以配置 MobX 来禁用此行为。查看 enforceActions
部分。例如,这在单元测试设置中非常有用,其中警告并不总是有多大价值。
¥By default, MobX 6 and later require that you use actions to make changes to the state.
However, you can configure MobX to disable this behavior. Check out the enforceActions
section.
For example, this can be quite useful in unit test setup, where the warnings don't always have much value.