【免费下载链接】visual-jsonThe Visual JSON Editor. Schema-aware, embeddable, extensible.项目地址https://gitcode.com/gh_mirrors/vis/visual-json点击查看免费下载visual-json 是一款支持 Schema 感知、可嵌入、可扩展的可视化 JSON 编辑器The Visual JSON Editor而它的 undo/redo撤销/重做能力核心就来自 visual-json/core 包里的一个仅 42 行的History类。本文将带你走读 history.ts 的源码看看它如何用单栈 游标的经典设计让编辑器的每一步操作都能随心撤销和重做。 为什么 JSON 编辑器离不开 undo/redo在 JSON 编辑器里用户每天都在做这类操作修改某个字段的值增删、重命名属性拖拽移动、调整数组顺序批量变更节点类型一旦误删了一个节点没有撤销功能就意味着满盘皆输。visual-json 把这类需求抽象得非常干净所有树操作都是不可变的immutable每次操作都会产出一个全新的TreeState而History类只需要负责存档这些快照、并在前后翻动。这种存状态而非存操作的思路是本文源码走读的第一个重点 ✨ 核心源码走读History 类只有 42 行History类定义在 packages/visual-json/core/src/history.ts并通过 index.ts 从visual-json/core对外导出。整个类只有两个私有字段export class History { private stack: TreeState[] []; private cursor -1; // ... }这就是经典的单栈 游标模型成员含义stack存放所有历史快照TreeState的数组cursor指针标记当前所在的历史位置初始为 -1还没有任何状态理解了这个指针undo 和 redo 就一目了然了 单栈 游标undo/redo 一步到位很多初学者会以为撤销重做需要两个栈一个 undo 栈、一个 redo 栈。visual-json 只用了一个数组数组cursor之前的部分 可以撤销的历史数组cursor之后的部分 可以重做的未来undo(): TreeState | null { if (!this.canUndo) return null; this.cursor--; return this.stack[this.cursor]; } redo(): TreeState | null { if (!this.canRedo) return null; this.cursor; return this.stack[this.cursor]; }undo 就是游标减一redo 就是游标加一源码见 history.ts#L19-L29。两个能力判断同样简单get canUndo(): boolean { return this.cursor 0; } get canRedo(): boolean { return this.cursor this.stack.length - 1; }canUndo/canRedo这两个 getter 会直接驱动 UI 上撤销/重做按钮的可用状态——这正是编辑器体验流畅的关键细节。push() 方法剪掉 redo 分支 容量上限push是History里逻辑最密集的方法一共四行核心代码push(state: TreeState): void { this.stack this.stack.slice(0, this.cursor 1); this.stack.push(state); if (this.stack.length MAX_HISTORY) { this.stack.shift(); } else { this.cursor; } }拆开看它做了三件事剪掉 redo 分支slice(0, this.cursor 1)会丢弃游标之后的所有未来状态。这符合直觉——撤销两步之后又做了新操作原来那条未来路径就应该作废想象 Git 里一次新的 commit 会让旧分支失焦。压入新快照把最新的TreeState追加到栈尾。容量保护栈超过MAX_HISTORY 100时shift()挤掉最老的一条。这里有个精妙之处——超限走shift()分支时不加cursor因为数组整体前移了一位游标的数值不变却自动指向了最新状态正常分支才需要cursor。 100 条快照的默认上限是一个很好的工程权衡既能覆盖绝大多数编辑场景又不会让内存随编辑时长无限增长。 框架层如何用 History以 React 为例History本身是纯 TS 类不依赖任何框架React / Vue / Svelte 三个封装包共用同一套逻辑。以 React 封装 visual-json.tsx 为例用法非常直接持有实例用useRefHistory(new History())保存跨渲染不重建每次变更就存档setTree更新树状态后立即historyRef.current.push(newTree)并同步canUndo/canRedo到 state 供 UI 使用见 visual-json.tsx#L123-L133undo/redo 回调拿到返回的TreeState后直接setTreeState并通过onChange通知外部见 visual-json.tsx#L135-L155快捷键开箱即用Ctrl/Cmd Z撤销、Shift Ctrl/Cmd Z或Ctrl Y重做见 visual-json.tsx#L157-L174外部值变化时重置历史如果外部valueprop 变了会新建一个History并只压入初始快照保证历史栈不被跨文档污染见 visual-json.tsx#L113-L116。Vue 与 Svelte 封装也是同样的模式VisualJson.vue 与 VisualJson.svelte 里同样各持有一个new History()实例。 测试用例里藏着哪些设计细节官方为History写了 10 个测试用例见 history.test.ts其中有三个用例特别值得新手学习undo 之后 push 会丢弃 redo 栈L74-L90push 三次后 undo 再 push此时canRedo必须为false而继续 undo 一步回到的是最初状态而非被剪掉的状态——这正是slice剪枝行为的直接验证。超限裁剪后 current 依然正确L105-L135连续 push 101 次最老的快照被挤掉可撤销次数是 99 而不是 100current始终指向最新状态。空栈边界初始状态下undo()与redo()都安全地返回nullcurrent也为null不会抛错L15-L20。这套测试把游标越界、分支剪枝、容量裁剪三个最容易出 bug 的角落全部覆盖了是学习状态机类单元测试的绝佳范本。 想继续深入核心源码packages/visual-json/core/src/history.ts单元测试packages/visual-json/core/src/tests/history.test.tsTreeState类型定义packages/visual-json/core/src/types.ts#L24-L27Core API 文档含 History 用法示例apps/web/src/app/docs/core/page.mdx#L69-L83React 封装packages/visual-json/react/src/visual-json.tsx总结visual-json 的History类用 42 行代码演示了一个教科书级的 undo/redo 实现不可变树状态 快照栈存状态而非操作撤销重做天然可逆、零兼容成本单栈 游标undo/redo 各一行核心逻辑canUndo/canRedo直接驱动 UIpush 剪枝 100 条上限处理了撤销后新操作的分支问题和内存增长问题框架无关React、Vue、Svelte 三端复用同一个类行为完全一致。下次当你按下 Ctrl Z 时背后就是这段游标轻轻移动了一格。希望这篇源码走读能帮你把 undo/redo 的设计彻底想明白赞分享【免费下载链接】visual-jsonThe Visual JSON Editor. Schema-aware, embeddable, extensible.项目地址https://gitcode.com/gh_mirrors/vis/visual-json点击查看免费下载相关推荐Fluid Framework 中的 Undo/Redo 实现原理分布式协作的撤销重做机制详解Fluid Framework 中的 Undo/Redo 实现原理分布式协作的撤销重做机制详解 Fluid Framework 是一个用于构建分布式实时协作后端前端为 three.js Editor 的撤销/重做Undo/Redo扩展自定义命令Command为 three.js Editor 的撤销/重做Undo/Redo扩展自定义命令Command 本指南以 three.js 官方编辑器editor自3D渲染图形学前端TinyBase Checkpoints 使用指南为 Store 构建撤销 / 重做Undo / Redo能力TinyBase Checkpoints 使用指南为 Store 构建撤销 / 重做Undo / Redo能力 Checkpoints 是 TinyBas数据库后端前端上一篇VMAF特征选择算法使用递归特征消除优化模型输入维度下一篇PP-MobileSeg面向移动端的高效语义分割模型训练与部署实战PaddleSeg 实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考