- 状态管理
- 前端
【免费下载链接】mobx-state-tree
Full-featured reactive state management without the boilerplate
导读
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)。
属性语义
| 属性 | 类型 | 含义 |
|---|---|---|
patches | ReadonlyArray<IJsonPatch> | 录制期间累积的全部正向补丁(按发生顺序) |
inversePatches | ReadonlyArray<IJsonPatch> | 每个正向补丁对应的逆补丁(按原顺序排列) |
reversedInversePatches | ReadonlyArray<IJsonPatch> | 逆补丁数组的反转副本,可直接顺序应用以回滚变更 |
recording | boolean | 当前是否处于录制状态(只读 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 ): IPatchRecorderrecordPatches在 src/core/mst-operations.ts:177 导出,接受两个参数:
subject:要录制补丁的状态树节点;filter?(可选):过滤函数,返回false的补丁将被跳过不记录。该函数能同时拿到正向补丁、逆补丁以及当前动作上下文actionContext(由getRunningActionContext()提供),可用于按路径、操作类型或动作来源做精细化筛选。
内部工作流
从实现(src/core/mst-operations.ts:188-257)可以梳理出完整调用链:
- 初始化:内部维护
data.patches与data.inversePatches两个可变数组,以及一个按需生成不可变快照的publicData缓存对象; - 挂载监听器:
resume()内部调用onPatch(subject, (patch, inversePatch) => {...}),每次树发生变更时,onPatch回调都会立即收到patch与inversePatch对——注意onPatch的语义是立即触发、不等事务结束(见 docs/API/index.md:3853 的说明); - 过滤与累积:回调内先执行
filter(若提供),通过后把patch压入data.patches、把inversePatch压入data.inversePatches,同时将publicData缓存标记为脏(置undefined); - 按需缓存:
patches、inversePatches、reversedInversePatches都是 getter,仅在首次访问时通过slice()/slice().reverse()生成不可变副本,避免外部修改污染内部数据; - 返回并立即开始录制:函数末尾调用
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 行):
- 创建实例并调用
recordPatches(instance); - 执行变更,然后
recorder.stop(); - 断言
recorder.patches与recorder.inversePatches与预期一致; - 新建克隆,
recorder.replay(clone),断言克隆快照与变更后的原实例快照相等; - 调用
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
相关推荐
Apache Arrow GLib 示例代码实战指南:用 C、Lua、Vala 读写 Arrow 文件与流格式
Apache Arrow GLib 示例代码实战指南:用 C、Lua、Vala 读写 Arrow 文件与流格式 Apache Arrow 官方仓库在 c_gli
状态管理前端终极防撤回神器:RevokeMsgPatcher完整使用指南,让微信/QQ消息撤回失效
终极防撤回神器:RevokeMsgPatcher完整使用指南,让微信/QQ消息撤回失效 RevokeMsgPatcher是一款强大的PC版微信/QQ/TIM防撤
桌面应用即时通讯终极微信QQ防撤回神器:RevokeMsgPatcher完整使用指南,消息撤回也能看!
终极微信QQ防撤回神器:RevokeMsgPatcher完整使用指南,消息撤回也能看! RevokeMsgPatcher是一款强大的PC版微信/QQ/TIM防撤
桌面应用即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考