跳至主内容区

补丁机制

[非官方测试版翻译]

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

egghead.io lesson 14: Capture patches using _produceWithPatches_
egghead.io lesson 16: Apply Patches using _applyPatches_

⚠ 从版本 6 开始,必须通过在应用启动时显式调用 enablePatches() 来启用补丁支持。

在 producer 函数执行过程中,Immer 能够记录所有可重放修改操作的补丁。当您需要临时派生状态分支并将变更同步回原始状态时,这是极其强大的工具。

补丁机制在以下场景中尤为实用:

  • 与其他方交换增量更新(例如通过 WebSocket)

  • 用于调试/追踪,精确观察状态随时间的变化

  • 作为撤销/重做功能的基础,或在略有差异的状态树上重放变更

applyPatches 可便捷地重放补丁。以下示例演示了如何记录增量更新并应用其反向操作:

import {produce, applyPatches} from "immer"

// version 6
import {enablePatches} from "immer"
enablePatches()

let state = {
name: "Micheal",
age: 32
}

// Let's assume the user is in a wizard, and we don't know whether
// his changes should end up in the base state ultimately or not...
let fork = state
// all the changes the user made in the wizard
let changes = []
// the inverse of all the changes made in the wizard
let inverseChanges = []

fork = produce(
fork,
draft => {
draft.age = 33
},
// The third argument to produce is a callback to which the patches will be fed
(patches, inversePatches) => {
changes.push(...patches)
inverseChanges.push(...inversePatches)
}
)

// In the meantime, our original state is replaced, as, for example,
// some changes were received from the server
state = produce(state, draft => {
draft.name = "Michel"
})

// When the wizard finishes (successfully) we can replay the changes that were in the fork onto the *new* state!
state = applyPatches(state, changes)

// state now contains the changes from both code paths!
expect(state).toEqual({
name: "Michel", // changed by the server
age: 33 // changed by the wizard
})

// Finally, even after finishing the wizard, the user might change his mind and undo his changes...
state = applyPatches(state, inverseChanges)
expect(state).toEqual({
name: "Michel", // Not reverted
age: 32 // Reverted
})

生成的补丁类似于 RFC-6902 JSON Patch 标准,但存在关键差异:path 属性使用数组而非字符串。这简化了补丁处理流程。如需符合官方规范,执行 patch.path = patch.path.join("/") 即可转换。以下是典型补丁及其反向操作的示例:

[
{
"op": "replace",
"path": ["profile"],
"value": {"name": "Veria", "age": 5}
},
{"op": "remove", "path": ["tags", 3]}
]
[
{"op": "replace", "path": ["profile"], "value": {"name": "Noa", "age": 6}},
{"op": "add", "path": ["tags", 3], "value": "kiddo"}
]

⚠ 注意:Immer 生成的补丁集能确保正确性——即应用到相同基础对象会产生相同终态。但 Immer 不保证补丁集的最优化(即最小补丁数量)。"最优"标准因场景而异,且计算最优补丁集可能开销巨大。因此建议对生成的补丁进行后处理,或参照下文方法进行压缩。

produceWithPatches

egghead.io lesson 19: Using inverse patches to build undo functionality
egghead.io lesson 20: Use patches to build redo functionality

相比设置补丁监听器,更简便的方式是使用 produceWithPatches。其函数签名与 produce 相同,但返回值变为三元组 [nextState, patches, inversePatches]。与 produce 类似,produceWithPatches 同样支持柯里化。

import {produceWithPatches} from "immer"

const [nextState, patches, inversePatches] = produceWithPatches(
{
age: 33
},
draft => {
draft.age++
}
)

执行结果示例:

[
{
age: 34
},
[
{
op: "replace",
path: ["age"],
value: 34
}
],
[
{
op: "replace",
path: ["age"],
value: 33
}
]
]

深入理解请参阅:使用 Immer 分发补丁与重基操作

技巧提示:参考此方案压缩历史补丁