2026/5/14 3:37:18
网站建设
项目流程
宿州微网站建设,巩义网站建设模板,网站打开速度太慢,十种网络推广的方法我有一支技术全面、经验丰富的小型团队#xff0c;专注高效交付中等规模外包项目#xff0c;有需要外包项目的可以联系我撤销#xff08;Undo#xff09;这种功能#xff0c;看起来很简单#xff1a;点一下回到上一步嘛。 但你真做过就知道#xff0c;它最擅长的不是“回…我有一支技术全面、经验丰富的小型团队专注高效交付中等规模外包项目有需要外包项目的可以联系我撤销Undo这种功能看起来很简单点一下回到上一步嘛。 但你真做过就知道它最擅长的不是“回退”而是悄悄把你的状态系统炸成一团。我做过好几次 undo 栈最早那种指针方案基本都活不到上线就开始报undefined到了第三次我决定换思路——做一个轻量、但很难写崩的方案。我的目标很明确不要任何“指针 下标运算”只要纯粹、可预测的数据流出错面尽量小于是我选了一个老而稳的思路Undo/Redo 用两条独立的栈来表示。为什么你需要一个“极简撤销栈”只要你的应用允许用户修改数据改文字、拖拽、加条目、删记录、改设置……你几乎都需要撤销/重做。一般有两种模式版本历史Version history像 Photoshop 时间线那样可以回到任何历史节点撤销栈Undo stack线性的撤一步、再撤一步重做也是线性的现实里绝大多数产品只需要第二种。那问题就变成怎么让这个“线性撤销”又简单又可靠指针方案的问题你迟早会踩到越界很多实现会用一个数组 一个指针push指针前进undo指针后退redo指针再前进听起来很合理。 但在 JS 里它很容易出现“指针漂移”这种阴间 bug指针没更新对redo 历史没清干净指针越界后读到了不存在的 index然后你就开始看见cannot read property ... of undefined我不想再追着 index 跑了。于是我把它拆成两条栈past过去可撤销future未来可重做双栈最干净的 Undo/Redo 结构双栈的核心动作特别像“倒沙子”执行新动作放进past并清空future因为未来已经被你改写了undo从past弹出执行 undo再放进futureredo从future弹出执行 do再放回past代码长这样保留原结构、只做小幅调整让逻辑更顺function createUndoStack() { let past []; let future []; return { push(doFn, undoFn) { doFn(); past.push({ doFn, undoFn }); // 新动作会抹掉所有可重做的历史 future.length 0; }, undo() { const action past.pop(); if (action) { action.undoFn(); future.unshift(action); } }, redo() { const action future.shift(); if (action) { action.doFn(); past.push(action); } }, get canUndo() { return past.length 0; }, get canRedo() { return future.length 0; } }; }这套方案的好处是完全没有指针。所以也就不会有“指针跑偏导致访问越界”的经典事故。缺点是会多占一点内存。 但换来的是更简单、更可读、更不容易写崩。但还有一个暗坑闭包会“偷走你的最新状态”上面那套写法有个非常常见的陷阱作用域捕获closure capture。在 JavaScript 里函数定义在另一个函数内部会保留外层变量的引用。 这意味着你以为保存的是“当时的数据”实际可能在 undo 时读到的是“后来变化后的数据”。结果就会出现一种很恶心的现象你点了 undo但恢复出来的不是当时的状态而是某个“被更新过的版本”。解决方案很直接在 push 的那一刻把你需要的数据克隆下来。现代 JS 很方便用structuredClone()直接深拷贝参数让 do/undo 永远拿到同一份“冻结的输入”。加上 structuredClone把撤销做成“稳到离谱”下面是更稳的一版保留你原本结构仍然是双栈只把数据捕获变成克隆function createUndoStack() { const past []; const future []; return { push(doFn, undoFn, ...withArgumentsToClone) { const clonedArgs structuredClone(withArgumentsToClone); const action { doWithData() { doFn(...clonedArgs); }, undoWithData() { undoFn(...clonedArgs); } }; action.doWithData(); past.push(action); future.length 0; }, undo() { const action past.pop(); if (action) { action.undoWithData(); future.unshift(action); } }, redo() { const action future.shift(); if (action) { action.doWithData(); past.push(action); } }, get undoAvailable() { return past.length 0; }, get redoAvailable() { return future.length 0; }, clear() { past.length 0; future.length 0; return true; } }; }逻辑还是那套逻辑但现在每个 action 都带着一份“当时就定格”的参数快照。 你不再怕变量后续变化造成“撤销时回不去”。使用示例像按 CtrlZ 一样简单const items []; const undoStack createUndoStack(); // add 1 undoStack.push( (v) items.push(v), // doFn () items.pop(), // undoFn 1 // 会被克隆并在 do/undo 中复用 ); console.log(items); // add 2 undoStack.push( (v) items.push(v), () items.pop(), 2 ); console.log(items); // add 3 undoStack.push( (v) items.push(v), () items.pop(), 3 ); console.log(items); // [1, 2, 3] // undo (remove 3) undoStack.undo(); console.log(items); // [1, 2] // redo (add 3 back) undoStack.redo(); console.log(items); // [1, 2, 3] undoStack.undo(); // [1, 2] undoStack.undo(); // [1] undoStack.redo(); // [1, 2] // 清空历史 undoStack.clear(); console.log(items); // [1, 2]每个动作的数据只在 push 时克隆一次之后无论 undo/redo 都能安全复现。 没有陈旧引用也没有“闭包偷换状态”的诡异问题。最终结论想把 Undo 做到“稳定、简单、可预测”你只需要两件事用双栈代替指针数组past / future 让 undo/redo 像倒沙子一样流动彻底告别指针漂移push 时克隆参数用structuredClone()把数据快照锁死避免闭包拿到“最新值”导致回不去这套方案紧凑、好读、耐操而且你调试时不会突然被它背刺。下次项目里你要做撤销先试试这个双栈写法。 一旦用顺了你会发现自己再也不想回到“数组 指针”那条路了。全栈AI·探索涵盖动效、React Hooks、Vue 技巧、LLM 应用、Python 脚本等专栏案例驱动实战学习点击二维码了解更多详情。最后CSS终极指南Vue 设计模式实战指南20个前端开发者必备的响应式布局深入React:从基础到最佳实践完整攻略python 技巧精讲React Hook 深入浅出CSS技巧与案例详解vue2与vue3技巧合集