Skip to content
目录

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 做了四件事:

  1. 初始化 e.history
  2. 重写 e.apply(保存/合并/分批策略)
  3. 提供 e.undo(逆序 inverse 回放)
  4. 提供 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.withMerging
  • HistoryEditor.withoutMerging
  • HistoryEditor.withoutSaving
  • HistoryEditor.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 手动定义撤销粒度。

MIT License.