一个 describe() 让撤销栈多占 18MB:命令模式省内存这条共识的实测复盘
翻十篇讲撤销重做的文章会看到十份几乎一样的结论表格全量快照实现简单但内存爆炸命令模式只存 diff 所以轻量因此编辑器应该用命令模式。这个结论是对的。但它省略了一个前提而这个前提在真实工程里经常不成立——我在一个画布工具上把四种历史栈的内存实测了一遍发现给命令对象多加一个看起来完全无害的describe()方法只读取一个数组的length200 步撤销栈就从 51KB 涨到了 18.4MB相差 372 倍。这个数字已经逼近同规模下内存爆炸的全量快照方案。换句话说命令模式省的不是内存是你没写错的时候的内存。背景这条共识缺了哪个前提主流建议把方案分成两类快照法Memento存整个状态命令法Command存动作差分。差分当然比全状态小所以命令模式赢。但这个推理里有个隐含假设命令对象的大小 ≈ 它声明的那几个字段的大小。在 JavaScript 里这个假设很脆弱。命令模式的教科书写法是让do()和undo()作为闭包捕获前后值function moveLayerCommand(layer, before, after) { return { label: 移动图层, do() { layer.x after; }, undo() { layer.x before; }, }; }闭包捕获的粒度不是变量而是作用域上下文Context。同一个作用域里创建的所有闭包共享同一个 Context 对象只要有一个闭包引用了某个变量这个变量就会被写进 Context然后被所有兄弟闭包一起钉住。而撤销栈的定义就是长期持有这些闭包。两者一叠加就有了本文要测的东西。实验设计四种历史栈同一份画布状态被测状态模拟一个矢量画布{ layers: [...] }每个图层是 12 个字段的普通对象id、name、x、y、w、h、rotation、opacity、fill、stroke、locked、visible。编辑动作固定为拖拽改一个图层的 x连续 200 步。四种历史栈实现方案每步存什么snapshotstructuredClone(state)全量深拷贝command带do()/undo()闭包的命令对象教科书写法command-plain纯数据命令{id, before, after}由外部执行器解释structural结构共享只复制 layers 数组和被改的那个图层其余共享引用内存测量方式是连续两次global.gc()后取process.memoryUsage().heapUsed的前后差值并对每种方案先跑一次小规模预热以排除 JIT 噪声。每个方案跑在独立进程里避免相互污染。图1四种方案的内存增长模型。快照与结构共享随状态规模 O(N) 增长命令模式与状态规模无关常数但常数项取决于闭包捕获了什么。复现命令# Node v22.22.2需要 --expose-gc for N in 50 500 5000; do for M in snapshot command command-plain structural; do node --expose-gc history-mem.js $M $N 200 done done第一组数据共识在大状态下确实成立先说结论主流建议在大状态下不仅成立而且差距比多数文章描述的还要夸张。200 步编辑后历史栈保留的堆内存每步平均字节图层数snapshotstructuralcommandcommand-plain5012,523 B644 B226 B77 B500127,323 B4,244 B226 B77 B50001,279,333 B40,253 B226 B77 B换算成 200 步的总占用5000 图层下全量快照保留244 MB结构共享 7.68 MB命令模式 44 KB纯数据命令 15 KB。244MB 对 44KB差 5600 倍。比内存更值得注意的是写入延迟。同一组测试里 200 步的构建耗时图层数snapshot 每步structural 每步500.052 ms—5000.505 ms—50005.68 ms0.021 ms5000 图层下每次编辑要花 5.68ms 做深拷贝。60fps 的一帧预算是 16.7ms这一下就吃掉 34%——而这还只是记录历史没算重绘。拖拽掉帧的锅很多时候在撤销栈上。第二组数据结构共享的交叉点在 29 个图层结构共享常被当作快照法的优化版一笔带过但它的内存模型和全量快照不同每步只多分配一个 layers 数组N 个指针和一个新图层对象。所以它是 O(N) 但常数极小。而命令模式是 O(1)——与状态规模完全无关。两条线必然相交。把图层数从 4 扫到 64图层数commandstructural4226 B44 B8226 B73 B16226 B130 B24226 B187 B28226 B216 B30226 B230 B64226 B474 B交叉点落在29 个图层附近。图2命令模式常数 226 B/步与结构共享随图层数线性增长的交叉点在 29 个图层附近低于此规模结构共享反而更省。这个数字本身不重要——它取决于图层对象大小和命令对象字段数。重要的是交叉点存在而且位置很低。一个表单构建器、一个配色方案编辑器、一个只有十几个节点的流程图状态规模都在交叉点以下。在这类工具里选命令模式付出了每个动作写一个类的工程成本换来的是更差的内存表现。顺带测了另一个方向的代价——从当前位置跳回 200 步之前图层数命令逐条回放结构共享取索引5000.0068 ms0.000055 ms50000.0040 ms0.000020 ms结构共享快 100 倍以上因为它是数组下标寻址而命令模式必须逐条undo()回放。但绝对值都在微秒级200 步深度下这个差距对用户不可感知不构成选型依据。诚实地说这一项是平局。第三组数据一个 describe() 让内存涨了 372 倍现在回到开头那个反例。真实的拖拽操作会产生一次性的中间数据——指针轨迹采样、事件对象、临时路径点。测试里模拟为每步生成 2000 个采样点约 94KB这份数据用完就该被回收。三个变体唯一区别是命令对象上多了什么方法// 变体 Alean —— do/undo 只引用 before/after/layer const cmd { label: 移动图层, do() { layer.x after; }, undo() { layer.x before; }, }; // 变体 Bdescribe —— 多一个方法只读了 sample.points.length const cmd { label: 移动图层, do() { layer.x after; }, undo() { layer.x before; }, describe() { return 拖拽采样 sample.points.length 点; }, }; // 变体 Cplain —— 纯数据命令采样点数提前算成标量 h.push({ id, before, after, pointCount: sample.points.length });200 步后的实测结果变体撤销栈保留每步相对 leanAlean51 KB259 B1×Bdescribe18,832 KB96,418 B372×Cplain14 KB74 B0.29×变体 A 证明了 V8 做得不错sample虽然在作用域里但没有任何闭包引用它所以它没进 Context正常回收。变体 B 只多了一个方法而且那个方法只读了.length这一个数字。但sample因此被写进 Contextdo()和undo()作为兄弟闭包共享同一个 Context——于是整份 94KB 的采样点被撤销栈钉住 200 份涨到 18.4 MB。对照第一组数据18.4 MB 已经接近 500 图层全量快照的 24.3 MB。号称省内存的方案因为一行 UI 提示文案退化到了它要取代的那个方案的量级。图3同一作用域内的闭包共享一个 Context 对象。describe() 引用 sample 后sample 被写入 Contextdo()/undo() 也随之持有它整份采样数据被撤销栈长期滞留。变体 C 是解法命令只存标量执行逻辑交给一个集中式执行器按type分发。没有闭包就没有 Context命令对象的大小回到它字面上声明的那几个字段。局限这次实测没有覆盖什么测量方法是近似的。heapUsed差值不等于精确的 retained size小规模数值有几 KB 级噪声这也是为什么交叉点只给29 附近而非精确值。要定位真实项目的滞留链还是得用 heap snapshot 看 retainer 路径。只测了单引擎单场景。Node v22.22.2 / Windows单一编辑动作、单一状态形状。Context 分配策略是 V8 实现细节不同版本、不同引擎JSC、SpiderMonkey结果可能不同。没有测合并coalescing和持久化。真实撤销栈会把连续同类操作合并这会同时降低所有方案的步数跨会话持久化则是快照系的主场命令模式需要额外的序列化设计。结构共享用的是朴素实现。只做了一层数组 slice没上 Immer / 持久化数据结构。真正的 HAMT/RRB-Tree 在大 N 下会比线性 slice 好得多交叉点会右移。闭包不是原罪。变体 A 说明只要作用域干净闭包命令的 226 B/步完全可用。问题出在作用域里混了大对象而这在拖拽、粘贴、导入这类动作里非常常见。结论与下一步一句话方法论撤销栈的选型不看命令 vs 快照看状态规模在交叉点哪一侧和作用域里有没有大对象。落成可执行的清单状态小于约 30 个实体表单、配色器、小型流程图→ 用结构共享快照。实现比命令模式简单一个数量级内存还更低回溯是 O(1)。状态大或持续增长画布、文档、低代码编排→ 用命令模式但写成纯数据命令 集中式执行器不要do()/undo()闭包。这次实测里纯数据命令是 74–77 B/步是闭包写法的 1/3。必须用闭包命令时隔离作用域。把命令构造放进只接收标量参数的独立函数别让它和临时大对象共享作用域。把撤销栈的内存写进测试。加一条200 步后堆增长不超过 X的断言比任何 code review 都可靠——describe()这类改动在 diff 里看起来完全无害。第 4 条是我这次最大的收获这个 372 倍的退化不会有任何 lint 规则报警不会让功能出错只会让用户在编辑半小时后觉得这网页越用越卡。本文的四个基准脚本history-mem.js/history-extra.js/history-capture.js都是零依赖单文件node --expose-gc直接跑欢迎在自己的状态形状上复测——交叉点会挪但结论的形状不会变。开源地址矩阵门户GitHub - wangzifan396-wzf/WB: 一个标签页收纳你全部的开发者工具。24 个单文件、零依赖、本地优先的开源工具下载即用。 · GitHub单文件工具聚合器GitHub - wangzifan396-wzf/nano-workbench: Single-file tabbed launcher for the nano-tools matrix - one tab, all 28 tools, instant switch. Zero-dep. Part of nano-tools. · GitHubGitHub 组织主页wangzifan396-wzf (WangZi) · GitHub

相关新闻

097、半桥LLC的变压器设计差异

097、半桥LLC的变压器设计差异

097、半桥LLC的变压器设计差异 上个月调试一块300W的LLC电源,客户反馈满载时效率只有91%,谐振频率附近居然还出现了明显的啸叫。拆开一看,变压器用的是反激时代的“老经验”——三明治绕法、磁芯中柱磨气隙、初级匝数按最低输入电压算。结果谐振电流波形畸变得厉害,开关管…

2026/8/2 3:37:47 阅读更多 →
数字通信解调:硬判决与软判决原理、性能对比与工程选型指南

数字通信解调:硬判决与软判决原理、性能对比与工程选型指南

1. 从“猜硬币”到“读心术”:理解数字解调判决的本质刚入行搞通信那会儿,老师傅总说,解调这活儿,就是从一堆被噪声“糊”了的信号里,把人家原本想说的“0”和“1”给认出来。这听起来就像在嘈杂的菜市场里听清别人喊的…

2026/8/2 3:37:47 阅读更多 →
Fay-UE5数字人开发实战:从AI驱动到实时3D交互全流程解析

Fay-UE5数字人开发实战:从AI驱动到实时3D交互全流程解析

1. 项目概述:Fay-UE5数字人工程是什么?最近在数字人开发圈子里,Fay-UE5这个组合的热度持续攀升。简单来说,这是一个将开源的Fay数字人控制框架与虚幻引擎5(UE5)深度整合的实战项目。它解决的核心痛点&#…

2026/8/2 3:36:47 阅读更多 →

最新新闻

UE5多边形退化问题:成因、诊断与修复全攻略

UE5多边形退化问题:成因、诊断与修复全攻略

1. 项目概述:当UE5对你说“多边形退化”在虚幻引擎5(UE5)里折腾过3D美术资源的朋友,大概率都见过这个让人心头一紧的报错:“多边形退化”(Degenerate Polygon)。这玩意儿不像编译错误那样有明确…

2026/8/2 21:55:16 阅读更多 →
I2C协议深度解析:从两根线到多设备通信的硬件与调试实践

I2C协议深度解析:从两根线到多设备通信的硬件与调试实践

1. 从两根线开始:I2C协议的设计哲学如果你拆开过任何一块现代电子设备的主板,无论是手机、电脑还是智能家居设备,你大概率会看到各种芯片之间通过密密麻麻的细线连接。但你是否想过,如果每个芯片之间通信都需要十几根甚至几十根线…

2026/8/2 21:55:16 阅读更多 →
URP屏幕抓取:实现动态扭曲、镜面反射与全局模糊效果

URP屏幕抓取:实现动态扭曲、镜面反射与全局模糊效果

1. 项目概述:为什么要在URP里“抓”屏幕?在Unity里做特效,尤其是那些需要“看到”整个屏幕画面才能工作的效果,比如水面的扭曲折射、光滑的镜面反射,或者全屏的景深模糊,我们经常听到一个词叫“屏幕抓取”。…

2026/8/2 21:55:16 阅读更多 →
虚幻引擎资产管理利器:Epic Asset Manager 核心功能与实战指南

虚幻引擎资产管理利器:Epic Asset Manager 核心功能与实战指南

1. 项目概述:为什么我们需要一个专门的资产管家?如果你是一名Unreal Engine的开发者,无论是独立游戏制作人、小型工作室成员,还是大型团队中的一员,我敢打赌,你的项目文件夹里一定塞满了各种各样的资产。从…

2026/8/2 21:55:16 阅读更多 →
RamFuzz:融合AI的智能模糊测试,提升单元测试的代码健壮性

RamFuzz:融合AI的智能模糊测试,提升单元测试的代码健壮性

1. 项目概述:当单元测试遇上模糊测试与AI如果你是一名开发者,尤其是负责后端服务、系统库或者对代码质量有较高要求的工程师,那么你一定对“测试”这个词又爱又恨。爱的是,一套健壮的测试用例是代码稳定运行的“定海神针”&#x…

2026/8/2 21:55:16 阅读更多 →
KMS_VL_ALL_AIO:Windows与Office智能激活技术的架构演进与实践指南

KMS_VL_ALL_AIO:Windows与Office智能激活技术的架构演进与实践指南

KMS_VL_ALL_AIO:Windows与Office智能激活技术的架构演进与实践指南 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 在数字化办公环境中,Windows操作系统和Office办公套件…

2026/8/2 21:54:16 阅读更多 →

日新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/2 0:00:38 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/2 0:00:38 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:38 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/2 0:00:38 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/2 0:00:38 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/2 0:00:38 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/2 6:34:16 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/2 2:47:48 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/2 0:23:22 阅读更多 →