MobX 🇺🇦

MobX 🇺🇦

  • API 参考
  • 中文
  • 한국어
  • 赞助商
  • GitHub

›技巧与窍门

介绍

  • 关于 MobX
  • 关于这份文档
  • 安装
  • MobX 的精髓

MobX 核心

  • 可观察状态
  • 动作
  • 计算值
  • 反应 {🚀}
  • API

MobX 和 React

  • React 集成
  • React 优化 {🚀}

技巧与窍门

  • 定义数据存储
  • 理解响应式
  • 子类化
  • 分析响应式 {🚀}
  • 带有参数的计算值 {🚀}
  • MobX-utils {🚀}
  • 自定义可观察对象 {🚀}
  • 延迟可观察对象 {🚀}
  • 集合工具 {🚀}
  • 拦截与观察 {🚀}

微调

  • 配置 {🚀}
  • 装饰器 {🚀}
  • 从 MobX 4/5 迁移 {🚀}
编辑

理解响应式

MobX 通常对您期望它响应的精确事物做出反应,这意味着在 90% 的使用案例中,MobX 应该“正常工作”。然而,在某些时候,您会遇到它没有按预期执行的情况。此时,了解 MobX 如何确定响应内容非常重要。

MobX 对在跟踪函数执行过程中读取的任何现有可观察属性做出反应。

  • “读取”是指解引用对象的属性,可以通过“点运算符”(例如 user.name)或使用方括号表示法(例如 user['name']、todos[3])或解构(例如 const {name} = user)来完成。
  • “跟踪函数”是指 computed 的表达式、observer React 函数组件的渲染、基于 observer 的 React 类组件的 render() 方法,以及作为第一个参数传递给 autorun、reaction 和 when 的函数。
  • “期间”是指仅跟踪在函数执行时读取的可观察对象。这些值是否被跟踪函数直接或间接使用并不重要。但是从函数中“衍生”出的事物不会被跟踪(例如 setTimeout、promise.then、await 等)。

换句话说,MobX 不会对以下内容做出反应:

  • 从可观察对象中获取的值,但在跟踪函数之外
  • 在异步调用的代码块中读取的可观察对象

MobX 跟踪属性访问,而不是值

为了用示例详细说明上述规则,假设您拥有以下可观察实例

class Message {
    title
    author
    likes
    constructor(title, author, likes) {
        makeAutoObservable(this)
        this.title = title
        this.author = author
        this.likes = likes
    }

    updateTitle(title) {
        this.title = title
    }
}

let message = new Message("Foo", { name: "Michel" }, ["Joe", "Sara"])

在内存中,它看起来如下所示。绿色框表示可观察属性。请注意,值本身不可观察!

MobX reacts to changing references

MobX 基本上做的是记录您在函数中使用的哪些箭头。之后,只要这些箭头发生变化,它就会重新运行;当它们开始引用其他内容时。

示例

让我们通过一些示例来展示这一点(基于上面定义的 message 变量)

正确:在跟踪函数内解引用

autorun(() => {
    console.log(message.title)
})
message.updateTitle("Bar")

这将按预期做出反应。.title 属性被 autorun 解引用,并在之后被更改,因此此更改被检测到。

您可以通过在跟踪函数内部调用 trace() 来验证 MobX 将跟踪什么。对于上述函数,它会输出以下内容

import { trace } from "mobx"

const disposer = autorun(() => {
    console.log(message.title)
    trace()
})
// Outputs:
// [mobx.trace] 'Autorun@2' tracing enabled

message.updateTitle("Hello")
// Outputs:
// [mobx.trace] 'Autorun@2' is invalidated due to a change in: 'Message@1.title'
Hello

也可以使用 getDependencyTree 获取内部依赖项(或观察者)树

import { getDependencyTree } from "mobx"

// Prints the dependency tree of the reaction coupled to the disposer.
console.log(getDependencyTree(disposer))
// Outputs:
// { name: 'Autorun@2', dependencies: [ { name: 'Message@1.title' } ] }

不正确:更改非可观察引用

autorun(() => {
    console.log(message.title)
})
message = new Message("Bar", { name: "Martijn" }, ["Felicia", "Marcus"])

这不会做出反应。message 被更改了,但 message 不是可观察对象,只是一个引用可观察对象的变量,但变量(引用)本身不可观察。

不正确:在跟踪函数之外解引用

let title = message.title
autorun(() => {
    console.log(title)
})
message.updateMessage("Bar")

这不会做出反应。message.title 在 autorun 之外被解引用,并且只包含解引用时 message.title 的值(字符串 "Foo")。title 不是可观察对象,因此 autorun 永远不会做出反应。

正确:在跟踪函数内解引用

autorun(() => {
    console.log(message.author.name)
})

runInAction(() => {
    message.author.name = "Sara"
})
runInAction(() => {
    message.author = { name: "Joe" }
})

这将对两种更改做出反应。author 和 author.name 都被点运算符访问,允许 MobX 跟踪这些引用。

请注意,我们必须在这里使用 runInAction 才能被允许在 action 之外进行更改。

不正确:在没有跟踪的情况下存储对可观察对象的本地引用

const author = message.author
autorun(() => {
    console.log(author.name)
})

runInAction(() => {
    message.author.name = "Sara"
})
runInAction(() => {
    message.author = { name: "Joe" }
})

第一个更改将被拾取,message.author 和 author 是同一个对象,并且 .name 属性在 autorun 中被解引用。但是,第二个更改不会被拾取,因为 message.author 关系没有被 autorun 跟踪。Autorun 仍然使用“旧”的 author。

常见陷阱:console.log

autorun(() => {
    console.log(message)
})

// Won't trigger a re-run.
message.updateTitle("Hello world")

在上面的示例中,更新后的消息标题不会被打印,因为它没有在 autorun 内部使用。autorun 仅依赖于 message,它不是可观察对象,而是一个变量。换句话说,就 MobX 而言,title 在 autorun 中没有使用。

如果您在 Web 浏览器调试工具中使用此工具,您可能仍然能够找到 title 的更新值,但这具有误导性 - autorun 最终在它被第一次调用时运行了一次。之所以会发生这种情况,是因为 console.log 是一个异步函数,对象会在稍后的时间被格式化。这意味着,如果您在调试工具栏中跟踪标题,您会找到更新后的值。但 autorun 不会跟踪任何更新。

使其正常工作的方法是确保始终将不可变数据或防御性副本传递给 console.log。因此,以下解决方案都将对 message.title 的更改做出反应

autorun(() => {
    console.log(message.title) // Clearly, the `.title` observable is used.
})

autorun(() => {
    console.log(mobx.toJS(message)) // toJS creates a deep clone, and thus will read the message.
})

autorun(() => {
    console.log({ ...message }) // Creates a shallow clone, also using `.title` in the process.
})

autorun(() => {
    console.log(JSON.stringify(message)) // Also reads the entire structure.
})

正确:在跟踪函数中访问数组属性

autorun(() => {
    console.log(message.likes.length)
})
message.likes.push("Jennifer")

这将按预期做出反应。.length 计入属性。请注意,这将对数组中的任何更改做出反应。数组不是按索引/属性(如可观察对象和映射)跟踪的,而是作为一个整体跟踪的。

不正确:在跟踪函数中访问超出界限的索引

autorun(() => {
    console.log(message.likes[0])
})
message.likes.push("Jennifer")

这将与上面的示例数据产生反应,因为数组索引计数被视为属性访问。但是,只有在提供的 `index < length` 时才会如此。MobX 不会跟踪尚未存在的数组索引。因此,请始终使用 `length` 检查来保护基于数组索引的访问。

正确:在跟踪函数中访问数组函数

autorun(() => {
    console.log(message.likes.join(", "))
})
message.likes.push("Jennifer")

这将按预期产生反应。所有不会更改数组的数组函数都会被自动跟踪。


autorun(() => {
    console.log(message.likes.join(", "))
})
message.likes[2] = "Jennifer"

这将按预期产生反应。所有数组索引赋值都会被检测到,但只有在 `index <= length` 时才会如此。

错误:"使用" Observable,但没有访问其任何属性

autorun(() => {
    message.likes
})
message.likes.push("Jennifer")

这将不会产生反应。仅仅因为 `likes` 数组本身没有被 `autorun` 使用,只有对数组的引用。因此,相反的是,`message.likes = ["Jennifer"]` 将会被捕获;该语句不会修改数组,而是 `likes` 属性本身。

正确:使用尚未存在的 Map 条目

const twitterUrls = observable.map({
    Joe: "twitter.com/joey"
})

autorun(() => {
    console.log(twitterUrls.get("Sara"))
})

runInAction(() => {
    twitterUrls.set("Sara", "twitter.com/horsejs")
})

这将产生反应。Observable Map 支持观察可能不存在的条目。请注意,这将最初打印 `undefined`。您可以使用 `twitterUrls.has("Sara")` 首先检查条目的存在性。因此,在没有 Proxy 支持动态键值集合的环境中,始终使用 Observable Map。如果您有 Proxy 支持,您也可以使用 Observable Map,但您也可以选择使用普通对象。

MobX 不会跟踪异步访问的数据

function upperCaseAuthorName(author) {
    const baseName = author.name
    return baseName.toUpperCase()
}
autorun(() => {
    console.log(upperCaseAuthorName(message.author))
})

runInAction(() => {
    message.author.name = "Chesterton"
})

这将产生反应。即使 `author.name` 没有被传递给 `autorun` 本身的函数取消引用,MobX 仍然会跟踪在 `upperCaseAuthorName` 中发生的取消引用,因为它发生在 `autorun` 执行期间。


autorun(() => {
    setTimeout(() => console.log(message.likes.join(", ")), 10)
})

runInAction(() => {
    message.likes.push("Jennifer")
})

这将不会产生反应,因为在 `autorun` 执行期间,没有访问 Observable,只有在 `setTimeout` 期间,而 `setTimeout` 是一个异步函数。

查看 异步操作 部分。

使用非 Observable 对象属性

autorun(() => {
    console.log(message.author.age)
})

runInAction(() => {
    message.author.age = 10
})

如果您在支持 Proxy 的环境中运行 React,这将产生反应。请注意,这仅针对使用 `observable` 或 `observable.object` 创建的对象执行。类实例上的新属性不会自动变为 Observable。

不支持 Proxy 的环境

这将不会产生反应。MobX 只能跟踪 Observable 属性,而 'age' 在上面没有被定义为 Observable 属性。

但是,可以使用 MobX 公开的 `get` 和 `set` 方法来解决这个问题。

import { get, set } from "mobx"

autorun(() => {
    console.log(get(message.author, "age"))
})
set(message.author, "age", 10)

[不支持 Proxy] 错误:使用尚未存在的 Observable 对象属性

autorun(() => {
    console.log(message.author.age)
})
extendObservable(message.author, {
    age: 10
})

这将不会产生反应。MobX 不会对在开始跟踪时不存在的 Observable 属性产生反应。如果两个语句交换位置,或者任何其他 Observable 导致 `autorun` 重新运行,则 `autorun` 将开始跟踪 `age`。

[不支持 Proxy] 正确:使用 MobX 工具读取 / 写入对象

如果您在不支持 Proxy 的环境中,并且仍然希望将 Observable 对象用作动态集合,可以使用 MobX 的 `get` 和 `set` API 来处理它们。

以下内容也将产生反应

import { get, set, observable } from "mobx"

const twitterUrls = observable.object({
    Joe: "twitter.com/joey"
})

autorun(() => {
    console.log(get(twitterUrls, "Sara")) // `get` can track not yet existing properties.
})

runInAction(() => {
    set(twitterUrls, { Sara: "twitter.com/horsejs" })
})

查看 集合工具 API 获取更多详细信息。

TL;DR

MobX 对在跟踪函数执行过程中读取的任何现有可观察属性做出反应。

← 定义数据存储子类化 →
  • MobX 跟踪属性访问,而不是值
  • 示例
MobX 🇺🇦
文档
关于 MobXMobX 的精髓
社区
GitHub 讨论 (NEW)Stack Overflow
更多
Star