slate-history 撤销重做与批处理策略
1. 数据结构先行
packages/slate-history/src/history.ts:
history.undos: Batch[]history.redos: Batch[]Batch = { operations: Operation[]; selectionBefore: Range | null }
关键点:历史单位是 batch,不是单个 operation。
2. withHistory 如何接管 editor
packages/slate-history/src/with-history.ts 做了四件事:
- 初始化
e.history - 重写
e.apply(保存/合并/分批策略) - 提供
e.undo(逆序 inverse 回放) - 提供
e.redo(顺序回放)
undo 核心:
batch.operations.map(Operation.inverse).reverse()
这是 operation 可逆性的直接应用。
源码(节选):
ts
const inverseOps = batch.operations.map(Operation.inverse).reverse()
for (const op of inverseOps) {
e.apply(op)
}3. merge/saving/split 的行为语义
默认保存规则
set_selection不进入 history(shouldSave返回 false)- 其他操作默认保存
默认合并规则
- 连续
insert_text同路径、offset 连续 -> merge - 连续
remove_text同路径、offset 相邻 -> merge - 同一轮操作中也可能并批(看
editor.operations.includes(lastOp))
源码(节选):
ts
if (op.type === 'insert_text' && prev.type === 'insert_text') {
return op.offset === prev.offset + prev.text.length && Path.equals(op.path, prev.path)
}显式控制工具(history-editor.ts)
HistoryEditor.withMergingHistoryEditor.withoutMergingHistoryEditor.withoutSavingHistoryEditor.withNewBatch
这些 API 是高阶命令可预测撤销体验的关键。
4. 撤销时为何还要管 selection
Batch.selectionBefore 在 undo/redo 中会参与恢复:
- undo 后恢复操作前选区
- redo 前可先设回 batch 起始选区
这也是很多历史 bug 的集中点:
5. 为什么有 withoutSaving
undo/redo 的回放本身会调用 apply,如果不关闭 saving,会把回放再次写入历史,造成历史污染或循环。
所以实现中明确包裹:
HistoryEditor.withoutSaving- 同时常配合
Editor.withoutNormalizing降低中间态干扰
6. 上限策略与实战影响
with-history.ts 里有 while (undos.length > 100) undos.shift()。
这意味着默认历史深度 100 批次,属于内存与体验的折中。
业务要更深历史需自行扩展 writeHistory 或自定义策略。
7. 可复述版本
Slate-history 不是简单数组 push/pop。
它把编辑行为分成 batch,批次里保存 operation 序列和起始 selection,undo 通过inverse + reverse回放。
合并策略默认只对连续文本输入友好,复杂命令要用withNewBatch/withMerging/withoutSaving手动定义撤销粒度。