Vue 3 富文本编辑器撤销重做应分层协作:reactive 作响应式状态容器,Delta 补丁或混合快照管理历史;每20步存全量快照,栈限50条Delta;绑定语义事件存档,还原时完整替换 reactive 对象。

Vue 3 中用 reactive 实现富文本编辑器的撤销重做,关键不是让 reactive 自己“记住历史”,而是把它当作实时状态容器,另建轻量、可控的历史栈来管理变更逻辑。直接对 reactive 对象深拷贝存快照虽可行,但内存压力大;纯靠监听输入事件压栈又易误触发。真正实用的方案是分层协作:视图层用 reactive 驱动响应式更新,历史层用 Delta 补丁或结构化快照按需存档。
用 Delta 补丁代替全量快照,省内存也保精度
每次用户完成一次有意义的操作(比如输入一段文字、插入一张图、删除一个段落),不保存整个编辑器内容,只记录一条不可变的变更指令:
-
INSERT:位置 142,插入 "
<img src="a.jpg">" - DELETE:位置 88,长度 12
- REPLACE:位置 205,旧 HTML 片段 → 新 HTML 片段
每条补丁仅几十字节,天然支持合并(连续输入自动聚合成单条 INSERT),且反向应用即可 undo。你只需维护一个 ref<Delta[]> 栈,所有操作都 push 进去,不碰 reactive 的原始数据。
快照 + Delta 混合策略,兼顾恢复速度与内存安全
纯 Delta 恢复逻辑稍复杂,而全量快照又撑不住高频编辑。生产环境推荐混合方式:
立即学习“前端免费学习笔记(深入)”;
使用 `@ainative/vue-sdk` 为 Vue 3 应用添加 AI 聊天功能。适用于以下场景:(1) 安装 `@ainative/vue-sdk`,(2) 在 Vue 组件中使用 `useChat` 组合式函数,(3) 提供……
- 每执行 20 步 Delta 后,主动用
structuredClone(toRaw(editorState))存一次全量快照,作为“基点” - undoStack 最多保留 50 条 Delta;超出时
shift()踢掉最老条目,但保留最近一次快照 - 执行 undo 时:若栈顶是 Delta,直接反向应用;若需跨基点(比如回退 30 步),先加载最近快照,再顺序重放后续 Delta
这样任意时刻最多只需加载 1 次快照 + 执行 ≤50 条补丁,既防 OOM,又避免卡顿。
操作边界要明确,避免误存和栈爆炸
不能在 @input 或 watch 里每改一个字符就存——这既违背用户直觉,也极易拖垮性能。应绑定到语义明确的结束事件:
- 使用 Tiptap:监听
editor.on('transaction')或自定义change事件 - 使用 WangEditor:监听
editor.on('change'),并在 handler 中调用save() - 手动实现时:在
blur、Enter、mouseup或工具栏按钮点击后触发存档
对拖拽、缩放、滑块等连续操作,加防抖(如 300ms 内只存最后一次),确保“一次操作”对应“一次压栈”。
状态替换必须干净,防止响应式污染
无论用快照还是 Delta,还原时都要完整替换 reactive 对象,而不是局部赋值:
- 快照还原:用
Object.assign(editorState, restoredSnapshot)或重建reactive(restoredSnapshot)(注意用shallowRef包裹快照,防 Vue 递归代理) - Delta 还原:直接修改
editorState.html或editorState.json字段,靠响应式自动刷新视图 - 切忌
editorState = reactive({...})—— 这会断开原有响应式连接,组件不再更新
所有历史数据(Delta 或快照)都应存为普通 JS 对象,不经过 reactive 或 ref 包装,只在需要还原时才用于驱动响应式状态。

















