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