• 编写异步 Actions (动作)
    • Promises
    • runInAction 工具函数
    • async / await
    • babel-plugin-mobx-deep-action
    • Generators & asyncAction

    编写异步 Actions (动作)

    action 包装/装饰器只会对当前运行的函数作出反应,而不会对当前运行函数所调用的函数(不包含在当前函数之内)作出反应!
    这意味着如果 action 中存在 setTimeout、promise 的 thenasync 语句,并且在回调函数中某些状态改变了,那么这些回调函数也应该包装在 action 中。创建异步 action 有几种方式。不能说某种方式一定比其他的好,本章只是列出编写异步代码的几种不同方式而已。
    我们先从一个基础的示例开始:

    Promises

    1. mobx.useStrict(true) // 不允许在动作之外进行状态修改
    2. class Store {
    3. @observable githubProjects = []
    4. @observable state = "pending" // "pending" / "done" / "error"
    5. @action
    6. fetchProjects() {
    7. this.githubProjects = []
    8. this.state = "pending"
    9. fetchGithubProjectsSomehow().then(
    10. projects => {
    11. const filteredProjects = somePreprocessing(projects)
    12. this.githubProjects = filteredProjects
    13. this.state = "done"
    14. },
    15. error => {
    16. this.state = "error"
    17. }
    18. )
    19. }
    20. }

    上面的示例会抛出异常,因为传给 fetchGithubProjectsSomehow promise 的回调函数不是 fetchProjects 动作的一部分,因为动作只会应用于当前栈。

    首选的简单修复是将回调函数变成动作。(注意绑定在这很重要,以获取正确的 this!):

    1. class Store {
    2. @observable githubProjects = []
    3. @observable state = "pending" // "pending" / "done" / "error"
    4. @action
    5. fetchProjects() {
    6. this.githubProjects = []
    7. this.state = "pending"
    8. fetchGithubProjectsSomehow().then(this.fetchProjectsSuccess, this.fetchProjectsError)
    9. }
    10. @action.bound // 回调动作
    11. fetchProjectsSuccess(projects) {
    12. const filteredProjects = somePreprocessing(projects)
    13. this.githubProjects = filteredProjects
    14. this.state = "done"
    15. }
    16. @action.bound // 回调动作
    17. fetchProjectsError(error) {
    18. this.state = "error"
    19. }
    20. }

    尽管这很整洁清楚,但异步流程复杂后可能会略显啰嗦。另外一种方案是你可以使用 action 关键字来包装 promises 回调函数。推荐这么做,但不是强制的,还需要给它们命名:

    1. mobx.useStrict(true) // 不允许在动作之外进行状态修改
    2. class Store {
    3. @observable githubProjects = []
    4. @observable state = "pending" // "pending" / "done" / "error"
    5. @action
    6. fetchProjects() {
    7. this.githubProjects = []
    8. this.state = "pending"
    9. fetchGithubProjectsSomehow().then(
    10. // 内联创建的动作
    11. action("fetchSuccess", projects => {
    12. const filteredProjects = somePreprocessing(projects)
    13. this.githubProjects = filteredProjects
    14. this.state = "done"
    15. }),
    16. // 内联创建的动作
    17. action("fetchError", error => {
    18. this.state = "error"
    19. })
    20. )
    21. }
    22. }

    runInAction 工具函数

    内联动作的缺点是 TypeScript 无法对其进行类型推导,所以你应该为所有的回调函数定义类型。
    你还可以只在动作中运行回调函数中状态修改的部分,而不是为整个回调创建一个动作。
    这种模式的优势是它鼓励你不要到处写 action,而是在整个过程结束时尽可能多地对所有状态进行修改:

    1. mobx.useStrict(true) // 不允许在动作之外进行状态修改
    2. class Store {
    3. @observable githubProjects = []
    4. @observable state = "pending" // "pending" / "done" / "error"
    5. @action
    6. fetchProjects() {
    7. this.githubProjects = []
    8. this.state = "pending"
    9. fetchGithubProjectsSomehow().then(
    10. projects => {
    11. const filteredProjects = somePreprocessing(projects)
    12. // 将‘“最终的”修改放入一个异步动作中
    13. runInAction(() => {
    14. this.githubProjects = filteredProjects
    15. this.state = "done"
    16. })
    17. },
    18. error => {
    19. // 过程的另一个结局:...
    20. runInAction(() => {
    21. this.state = "error"
    22. })
    23. }
    24. )
    25. }
    26. }

    注意,runInAction 还可以给定第一个参数作为名称。runInAction(f) 实际上是 action(f)() 的语法糖。

    async / await

    基于 async / await 的函数当开始使用动作时起初似乎会令人感到困惑。
    因为在词法上它们看起来是同步函数,它给人的印象是 @action 应用于整个函数。
    但事实并非若此,因为 async / await 只是围绕基于 promise 过程的语法糖。
    结果是 @action 仅应用于代码块,直到第一个 await
    在每个 await 之后,一个新的异步函数将启动,所以在每个 await 之后,状态修改代码应该被包装成动作。
    这正是 runInAction 再次派上用场的地方:

    1. mobx.useStrict(true) // 不允许在动作之外进行状态修改
    2. class Store {
    3. @observable githubProjects = []
    4. @observable state = "pending" // "pending" / "done" / "error"
    5. @action
    6. async fetchProjects() {
    7. this.githubProjects = []
    8. this.state = "pending"
    9. try {
    10. const projects = await fetchGithubProjectsSomehow()
    11. const filteredProjects = somePreprocessing(projects)
    12. // await 之后,再次修改状态需要动作:
    13. runInAction(() => {
    14. this.state = "done"
    15. this.githubProjects = filteredProjects
    16. })
    17. } catch (error) {
    18. runInAction(() => {
    19. this.state = "error"
    20. })
    21. }
    22. }
    23. }

    babel-plugin-mobx-deep-action

    如果你使用 babel,有一个插件在可以转译期间扫描 @action 方法并自动、正确地包装动作中的所有回调函数及 await 语句: mobx-deep-action。

    Generators & asyncAction

    最后,在 mobx-utils 包中还有一个 asyncAction 工具函数,它其实是使用 generators 来自动地在动作中包装 yield 过的 promises 。优点是它在语法上十分接近 async / await (使用不同的关键字),并且异步部分不需要手动包装成动作,从而代码非常整洁。
    只要确保每个 yield 返回 promise 。

    asyncAction 可以作为装饰器和函数使用 (就像 @action)。
    asyncAction 与 MobX 开发者工具结合很好,所以它可以很轻松的追踪异步函数的进程。
    想了解更多详情,请参见 asyncAction 的文档。

    1. import {asyncAction} from "mobx-utils"
    2. mobx.useStrict(true) // 不允许在动作之外进行状态修改
    3. class Store {
    4. @observable githubProjects = []
    5. @observable state = "pending" // "pending" / "done" / "error"
    6. @asyncAction
    7. *fetchProjects() { // <- 注意*号,这是一个 generator 函数!
    8. this.githubProjects = []
    9. this.state = "pending"
    10. try {
    11. const projects = yield fetchGithubProjectsSomehow() // 用 yield 代替 await
    12. const filteredProjects = somePreprocessing(projects)
    13. // 异步代码块会被自动包装成动作
    14. this.state = "done"
    15. this.githubProjects = filteredProjects
    16. } catch (error) {
    17. this.state = "error"
    18. }
    19. }
    20. }