☰
mobx-state-tree 补丁录制器 IPatchRecorder 详解:实现撤销/重做与补丁回放的完整指南
2026/10/7 9:57:12 网站建设 项目流程
  • 状态管理
  • 前端

【免费下载链接】mobx-state-tree

Full-featured reactive state management without the boilerplate

项目地址:https://gitcode.com/gh_mirrors/mo/mobx-state-tree
点击查看免费下载

导读

IPatchRecorder是 mobx-state-tree(MST)中与补丁(patch)录制功能直接对应的核心接口,由recordPatches函数创建并返回,用于把一棵状态树在一段时间内发生的所有变更以 JSON-Patch 序列的形式记录下来,并支持撤销(undo)、重放(replay)、暂停与恢复录制。本文将基于 IPatchRecorder 接口文档,结合recordPatches的源码实现与仓库测试用例,讲解该接口的全部属性与方法语义、底层调用链,以及如何用它实现撤销/重做、变更审计和状态回放等实战能力。

IPatchRecorder:一次录制的完整生命周期

接口定义与产生方式

IPatchRecorder定义在 src/core/mst-operations.ts:135,其完整类型签名如下:

export interface IPatchRecorder { patches: ReadonlyArray<IJsonPatch> inversePatches: ReadonlyArray<IJsonPatch> reversedInversePatches: ReadonlyArray<IJsonPatch> readonly recording: boolean stop(): void resume(): void replay(target?: IAnyStateTreeNode): void undo(target?: IAnyStateTreeNode): void }

该接口对象由recordPatches函数创建。正如源码注释所述(src/core/mst-operations.ts:146),它是"围绕onPatch与applyPatch的小型抽象":recordPatches向状态树挂载一个补丁监听器,把所有补丁及对应的逆补丁累积到内部数组中,并对外暴露上述属性与方法。从类型结构看,接口拥有4 个属性(patches、inversePatches、reversedInversePatches、recording)和4 个方法(stop、resume、replay、undo)。

属性语义

属性类型含义
patchesReadonlyArray<IJsonPatch>录制期间累积的全部正向补丁(按发生顺序)
inversePatchesReadonlyArray<IJsonPatch>每个正向补丁对应的逆补丁(按原顺序排列)
reversedInversePatchesReadonlyArray<IJsonPatch>逆补丁数组的反转副本,可直接顺序应用以回滚变更
recordingboolean当前是否处于录制状态(只读 getter)

其中补丁类型 IJsonPatch 遵循 JSON-Patch 规范(RFC 6902),定义于 src/core/json-patch.ts:7:

export interface IJsonPatch { readonly op: "replace" | "add" | "remove" readonly path: string readonly value?: any }

op只能是"replace" | "add" | "remove"三种操作,path是形如/text、/children/0/text的 JSON 路径,value仅在add与replace时携带。仓库还在 src/core/json-patch.ts:13 定义了扩展类型 IReversibleJsonPatch,额外带oldValue字段,用于生成逆补丁(内部通过splitPatch/invertPatch实现,见 src/core/json-patch.ts:21)。

方法语义与返回约定

方法签名行为
stop()(): void停止录制(移除补丁监听器)
resume()(): void恢复录制(重新挂载监听器)
replay(target?)(target?: IAnyStateTreeNode): void将已录制补丁应用到目标节点;省略target时应用到原主体
undo(target?)(target?: IAnyStateTreeNode): void逆序应用逆补丁,回滚变更;省略target时回滚原主体,且会停止录制

需要注意:undo()在撤销的同时会使录制器停止,这是源码中的既定行为(src/core/mst-operations.ts:251 的注释明确说明 "stops the recorder if not already stopped")。

源码实现:recordPatches 的底层机制

调用签名

export function recordPatches( subject: IAnyStateTreeNode, filter?: ( patch: IJsonPatch, inversePatch: IJsonPatch, actionContext: IActionContext | undefined ) => boolean ): IPatchRecorder

recordPatches在 src/core/mst-operations.ts:177 导出,接受两个参数:

  • subject:要录制补丁的状态树节点;
  • filter?(可选):过滤函数,返回false的补丁将被跳过不记录。该函数能同时拿到正向补丁、逆补丁以及当前动作上下文actionContext(由getRunningActionContext()提供),可用于按路径、操作类型或动作来源做精细化筛选。

内部工作流

从实现(src/core/mst-operations.ts:188-257)可以梳理出完整调用链:

  1. 初始化:内部维护data.patches与data.inversePatches两个可变数组,以及一个按需生成不可变快照的publicData缓存对象;
  2. 挂载监听器:resume()内部调用onPatch(subject, (patch, inversePatch) => {...}),每次树发生变更时,onPatch回调都会立即收到patch与inversePatch对——注意onPatch的语义是立即触发、不等事务结束(见 docs/API/index.md:3853 的说明);
  3. 过滤与累积:回调内先执行filter(若提供),通过后把patch压入data.patches、把inversePatch压入data.inversePatches,同时将publicData缓存标记为脏(置undefined);
  4. 按需缓存:patches、inversePatches、reversedInversePatches都是 getter,仅在首次访问时通过slice()/slice().reverse()生成不可变副本,避免外部修改污染内部数据;
  5. 返回并立即开始录制:函数末尾调用recorder.resume()后才返回,所以拿到 recorder 的那一刻录制已经生效。

recording属性是一个 getter,其值等价于内部监听器 disposer 是否存在(!!disposer)——stop()调用 disposer 并置空,resume()在已有监听器时直接返回不再重复挂载。

replay 与 undo 的底层实现

  • replay(target?):applyPatch(target || subject, data.patches),把累积的正向补丁原样应用到目标节点。若传入其他节点,可实现"把 A 上的变更序列同步到 B";
  • undo(target?):applyPatch(target || subject, data.inversePatches.slice().reverse()),即把逆补丁反转顺序后依次应用。这正是 docs/concepts/patches.md 中"补丁可被逆序应用,从而支撑撤销/重做等强大模式"这一理念的直接落地。

而applyPatch本身(src/core/mst-operations.ts:124)会先做参数校验(assertIsStateTreeNode、校验对象/数组),再调用底层节点的applyPatches(asArray(patch))批量应用。

实战用法:录制、重放与撤销

最小可用示例

import { types, getSnapshot, recordPatches } from "mobx-state-tree" const Todo = types.model("Todo", { title: types.string, done: false }) const todo = Todo.create({ title: "Read docs", done: false }) // 开始录制(调用后立即生效) const recorder = recordPatches(todo) // 通过 action 修改状态(保护模式下外部直接赋值会抛错,建议在 action 内变更) todo.toggleDone() // 假设模型中有 toggleDone action // 查看录制结果 console.log(recorder.patches) // [{ op: "replace", path: "/done", value: true }] console.log(recorder.recording) // true // 回滚到录制前的状态(同时停止录制) recorder.undo() console.log(getSnapshot(todo).done) // false

基于 filter 只录制关心的补丁

当状态树很大、只想关注某个子树或某类操作时,可传入过滤函数:

const recorder = recordPatches(todoStore, (patch, inversePatch, actionContext) => { // 只录制 /todos 路径下的变更 return patch.path.startsWith("/todos/") })

该 filter 的第三个参数actionContext来自getRunningActionContext(),因此你还可以按"发起变更的 action 名称"做筛选,实现按业务动作分类的审计日志。

把变更同步到另一棵树(replay 的典型场景)

const source = TodoStore.create({ todos: [] }) const target = TodoStore.create({ todos: [] }) const recorder = recordPatches(source) // ...执行一系列操作... recorder.stop() // 先停止录制,保证补丁序列完整 recorder.replay(target) // 将 source 上发生的变更原样同步到 target

这可用于状态同步、演示回放或跨实例复制等场景。

测试验证:接口行为与补丁语义的仓库证据

仓库在tests/core/recordPatches.test.ts 中对recordPatches及IPatchRecorder的完整行为做了系统性验证。测试的核心辅助函数testPatches呈现了标准的"录制 → 停止 → 重放 → 撤销"闭环(第 15-35 行):

  1. 创建实例并调用recordPatches(instance);
  2. 执行变更,然后recorder.stop();
  3. 断言recorder.patches与recorder.inversePatches与预期一致;
  4. 新建克隆,recorder.replay(clone),断言克隆快照与变更后的原实例快照相等;
  5. 调用recorder.undo(),断言原实例恢复到录制前的基线快照。

测试覆盖了四类结构上的补丁语义:

  • 简单属性替换(第 42-64 行):修改text字段产生{ op: "replace", path: "/text", value: "test" },逆补丁把value换回原值"Hi";
  • 数组的深层补丁(第 66-147 行):对children数组的更新、调和、新增、splice 删除,依次产生replace、add、remove补丁,且逆补丁顺序完全镜像;
  • Map 的深层补丁(第 149-245 行):验证children.get(...).text更新、put调和、set新对象、delete删除在 map 上的补丁表达;
  • 嵌套对象属性(第 247-353 行):对可选子对象child的更新、替换、置undefined的补丁行为。

这些用例同时验证了undo()在不传 target 时作用于原主体,以及replay()可作用于全新克隆节点的能力,是本文档所述接口行为最直接的实现级佐证。

与其他补丁 API 的关系

IPatchRecorder处于补丁体系的中层,前后各有协作方:

  • 底层是 onPatch 监听器与 applyPatch 应用器(均定义于 src/core/mst-operations.ts),recordPatches正是二者的组合封装;
  • 补丁数据模型是 IJsonPatch(op/path/value),内部逆补丁生成依赖 IReversibleJsonPatch 的oldValue字段;
  • 在它之上,你可以自由构建撤销/重做栈、补丁审计日志或"变更即同步"的协作功能。

与快照监听(onSnapshot)相比,补丁是细粒度的变更流:快照只在事务结束时整体通知一次,而补丁立即、逐个、可逆地反映每一次变更(docs/concepts/patches.md 明确列出这些差异)。IPatchRecorder的价值正在于把这条流变成可暂停、可回溯、可重演的"录制",让撤销/重做从手写逆操作中解放出来。

注意事项与最佳实践

  • 录制从调用即刻开始:recordPatches返回前会自动resume(),若需要排除某些前置变更,可在挂载前先准备好状态;
  • undo()会停止录制:撤销后如需继续录制,请显式调用resume();
  • patches等数组是只读不可变副本:内部通过slice()按需生成,外部修改不会影响录制器内部数据,也不会被后续补丁污染;
  • replay/undo的 target 参数:省略时作用于原主体;传入其他节点可实现跨树同步,但请确保目标节点的类型结构与补丁路径匹配;
  • filter 是录制期的"闸门":传入后即时生效,可用于排除噪声补丁,但注意被过滤的补丁也不会进入inversePatches,撤销范围会随之收窄;
  • 保护模式下的变更:状态树默认受保护,所有修改应通过 action 进行,录制本身不改变这一约束,非法直接赋值仍会抛错。

总结

IPatchRecorder是 mobx-state-tree 补丁体系中最实用的"录制器"抽象:patches/inversePatches/reversedInversePatches三个数组分别对应正向、逆向、逆序逆补丁三种视角,recording反映录制状态,stop/resume控制生命周期,replay/undo分别完成重放与回滚。其实现(src/core/mst-operations.ts:135-258)把onPatch与applyPatch组合成一个约 70 行的自洽模块,并配合tests/core/recordPatches.test.ts 中的结构级测试覆盖,为撤销/重做、变更审计、跨实例状态同步等需求提供了开箱即用且经过验证的解决方案。

  • 状态管理
  • 前端

【免费下载链接】mobx-state-tree

Full-featured reactive state management without the boilerplate

项目地址:https://gitcode.com/gh_mirrors/mo/mobx-state-tree
点击查看免费下载
上一篇:基于 .NET Aspire 编排 Semantic Kernel Process Framework:事件驱动业务流程实战指南
下一篇:Tasmota 内嵌 Berry 脚本引擎深度剖析:寄存器虚拟机、一次编译与标记-清除 GC 的嵌入式设计

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询