vue富文本编辑器撤销重做需存不可变快照:用toraw+structuredclone深拷贝后shallowref包裹入栈;双栈整块替换状态;绑定语义完成事件(如tiptap transaction、wangeditor change);栈长限50步并可配置。

Vue 的 reactive 状态本身不保存历史,要维护富文本编辑器的撤销重做栈,关键不是“监听变化”,而是**在用户完成一次有意义操作后,主动存下当前编辑内容的不可变快照**。直接把 reactive 对象 push 进栈会出问题——后续修改会污染历史记录。
快照必须脱离响应式系统
富文本内容(比如 editorContent)通常是一个 reactive 字符串或对象。存档时不能写 undoStack.push(state.editorContent),否则栈里存的是响应式引用,一改全改。
正确做法是:
- 用
toRaw(state)拿到原始 JS 数据 - 再用
structuredClone()深拷贝(现代浏览器支持,Safari 16.4+ 起可用) - 把拷贝结果用
shallowRef()包裹后入栈,避免 Vue 对快照做响应式代理,节省内存
双栈结构 + 替换式状态更新
撤销重做不是差量更新,而是整块状态跳转。每次 undo/redo 都要完整替换整个编辑内容,让 Vue 自动触发视图刷新:
- 保存:操作结束(如 blur、Enter、mouseup)调用
save()→undoStack.value.push(structuredClone(toRaw(state))) - 撤销:弹出
undoStack最后一项 → 把当前 state 拷贝推入redoStack→ 用弹出项重建state.editorContent = reactiveItem.editorContent - 重做:同理,从
redoStack弹出 → 当前 state 入undoStack→ 替换内容
结合富文本编辑器的实际触发时机
不能在输入过程中高频存档(比如 @input 每打一个字就 save),那样栈爆炸且违背用户直觉。应绑定到编辑器语义明确的“操作完成”事件上:
- Tiptap:监听
editor.on('transaction')或自定义change事件 - wangEditor 5:监听
editor.on('change'),并在回调中判断是否为用户主动提交(如非程序设置) - UEditor / vue-ueditor-wrap:监听
contentchange事件,并配合防抖(如 300ms 内无新变化再存) - TinyMCE:用
setup(editor) { editor.on('ExecCommand', () => save()) }捕获格式操作,再叠加Blur事件兜底
栈长度控制与内存安全
不限制栈大小会导致内存持续增长。建议默认上限 50 步,超出后自动移除最早一条:
- 每次
save()后检查:if (undoStack.value.length > MAX_HISTORY) undoStack.value.shift() -
redoStack同理处理,避免撤销后连续重做撑爆内存 - 可将最大步数作为 Composable 的参数(如
useUndo(state, { max: 30 })),便于不同编辑器实例差异化配置
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










