🛠️ CodePen实战:撤销重做功能全记录
🌟 目录
- 🚨 真实报错全记录 - 那些折磨我的Bug
- 🏗️ 极简架构设计 - 适合实验项目的结构
- 🧩 模块实现细节 - 关键代码解析
- 🚑 急救方案 - 快速Debug技巧
🚨 真实报错全记录
案例1:Vue的"温柔警告" 💛
[Vue warn]: Property "canRedo" was accessed during render but is not defined on instance.
🕵️ 现象:重做按钮偶尔消失
🔍 诊断过程:
- 检查模板中的
canRedo
拼写 ✅ - 发现setup()中漏返回属性:
// 错误代码
setup() {const canRedo = ref(false)// ...忘记return...
}
✅ 修复:
return {canRedo // 显式暴露给模板
}
案例2:幽灵报错 👻
[object Error] { message: "" }
🕵️ 现象:控制台只显示空错误对象
🔍 诊断:
- 添加错误边界:
window.onerror = (msg) => console.log('幽灵捕获:', msg)
- 发现异步操作未捕获异常:
// 错误代码
setTimeout(() => { throw new Error('test') }, 0)
✅ 修复:
// 所有异步操作包裹try-catch
setTimeout(() => {try { /* 操作代码 */ }catch(e) { console.error(e) }
}, 0)
案例3:重做按钮罢工 🚫
🕵️ 现象:点击重做无任何反应
🔍 诊断流程:
- 打印历史记录栈:
console.log('历史栈:', JSON.parse(JSON.stringify(history.stack)))
- 发现索引越界:
当前索引: 3 | 栈长度: 3
- 定位到redo方法:
// 错误代码
index.value += 1 // 当index=2时变成3,而长度是3
✅ 修复:
// 添加边界检查
index.value = Math.min(index.value + 1, stack.length - 1)
🏗️ 极简架构设计
系统流程图
模块职责
模块 | 职责 | 代码示例 |
---|---|---|
输入监听 | 捕获用户操作并防抖 | watch(text, debounceFn) |
历史管理 | 存储/检索编辑器状态 | history.push(snapshot) |
状态同步 | 保持DOM与数据一致 | nextTick(updateSelection) |
UI控制 | 按钮状态/快捷键处理 | :disabled="!canUndo" |
🧩 核心模块实现
历史管理器(精简版)
class History {constructor(max = 20) {this.stack = []this.index = -1 // 当前状态索引this.max = maxthis.lock = false // 防重入锁}// 🚨 关键方法:安全推送push(state) {if (this.lock) returnthis.lock = true// 裁剪后续记录this.stack.splice(this.index + 1)// 容量控制if (this.stack.length >= this.max) {this.stack.shift()this.index = Math.max(this.index - 1, -1)}this.stack.push(JSON.parse(JSON.stringify(state)))this.index = this.stack.length - 1setTimeout(() => this.lock = false, 50)}// 🚨 关键方法:安全撤销undo() {this.index = Math.max(this.index - 1, 0)return this.get()}// 🚨 关键方法:安全重做redo() {this.index = Math.min(this.index + 1, this.stack.length - 1)return this.get()}
}
状态同步器
// 🚨 DOM与数据同步
const syncSelection = () => {// 从DOM读取editor.selection.start = textarea.value.selectionStarteditor.selection.end = textarea.value.selectionEnd// 写入DOMnextTick(() => {textarea.value.selectionStart = editor.selection.starttextarea.value.selectionEnd = editor.selection.end})
}
🚑 急救Debug指南
场景1:操作后光标错位
快速检查:
- 是否在nextTick中更新选区?
- 快照是否包含selection数据?
- 是否存在CSS影响光标位置?
场景2:历史记录混乱
诊断步骤:
// 在push方法中添加日志
console.log('推送快照:', `内容长度: ${state.content.length}`, `光标: ${state.selection.start}-${state.selection.end}`
)
场景3:移动端失效
解决方案:
// 添加触摸事件监听
textarea.addEventListener('touchend', saveSelection)
📌 经验总结
- Vue响应式陷阱:直接存储响应式对象到历史栈会导致内存泄漏
- DOM时序问题:光标操作必须包裹在nextTick中
- 防抖重要性:300ms间隔能平衡性能和体验
- 边界检查:所有索引操作都要有Math.min/max保护
最后建议:在CodePen中开发时,每实现一个功能就添加console.log
检查点,比调试器更高效! 🐛🔍
完整可运行代码:https://codepen.io/RichardRourc/pen/bNGGJRV?editors=1111
github 仓库完整代码:https://github.com/RichardRourc/undo-redo/blob/main/undo%26redo.html 喜欢的点个赞哇
遇到新问题?随时截图发问,我会帮你分析! 💬