HTML Canvas save restore 状态保存与恢复新手必读

作者:技术拾光

时间:2026-06-06

浏览:634

许多初接触 Canvas 的开发者常对 save() 和 restore() 这对方法产生误解,认为它们能“撤销”已绘制的图形。实际上,这两个方法管理的并非像素,而是绘图上下文(即“画笔”)本身的状态。简单来说,它们负责控制“怎么画”,而非“画了什么”。若不成对使用,可能导致后续样式或位置错乱,严重

文章目录

许多初接触 Canvas 的开发者常对 save() 和 restore() 这对方法产生误解,认为它们能“撤销”已绘制的图形。实际上,这两个方法管理的并非像素,而是绘图上下文(即“画笔”)本身的状态。简单来说,它们负责控制“怎么画”,而非“画了什么”。若不成对使用,可能导致后续样式或位置错乱,严重时甚至抛出 DOMException: The operation is not supported 这样的运行时错误。

save() 和 restore() 保存/恢复哪些状态?

每次调用 save(),它都会将绘图上下文(CanvasRenderingContext2D)当前所有可变属性打包,压入一个内部状态栈。而 restore() 则从栈顶弹出一个状态包,并全量应用到当前上下文。

这个“状态包”具体包含以下四大类内容:

  • 变换矩阵:由 translate()、rotate()、scale()、transform() 等变换操作累积的效果。
  • 裁剪路径:通过 clip() 方法设置的绘制区域限制。
  • 样式属性:如填充色 fillStyle、描边色 strokeStyle、透明度 globalAlpha、线宽 lineWidth、字体 font、文本对齐方式 textAlign、阴影偏移 shadowOffsetX 等。
  • 合成模式:即 globalCompositeOperation,决定新图形如何与已有内容叠加。

需要明确的是,以下内容不会被保存:

  • 已绘制的图形:通过 fillRect()、stroke() 等方法画到 Canvas 上的像素,一旦落笔便无法通过 restore() 擦除或回退。
  • Canvas 元素尺寸:canvas 标签的 width 和 height 属性不受影响。
  • 像素数据:通过 getImageData() 获取或 putImageData() 放置的像素数据,与状态栈无关。

为什么 restore() 会报 DOMException?

此错误的根本原因只有一个:在状态栈为空时调用了 restore()。可以这样理解:每次 save() 向栈中放入一个盒子,restore() 则取出最上方的盒子。若栈已空而仍尝试取出,系统便会报错。

实际开发中,以下情况最易导致栈空错误:

  • 数量不匹配:restore() 调用次数多于 save()。例如只调用一次 save(),却连续调用三次 restore()。
  • 条件分支遗漏:在 if-else 或 switch 等分支中,仅某个分支调用了 save(),但所有分支出口都调用 restore(),导致某些路径下 restore() 缺少对应的 save()。
  • 异步调用陷阱:在 setTimeout、requestAnimationFrame 或事件回调等异步函数中调用 restore(),而外层同步代码可能已执行了 restore(),导致栈状态提前被清空。

调试此类问题时,可借助 Chrome 或 Firefox 开发者工具中的 Canvas Inspector 功能,它能实时显示当前上下文的状态栈深度,帮助快速定位 save() 与 restore() 是否配对。

嵌套 save/restore 的典型使用场景

该机制最经典的应用是模块化绘制。例如,封装一个绘制可旋转、可缩放图标的函数,并希望该函数的绘制过程完全独立,不影响调用前后的任何绘制状态。此时 save() 和 restore() 便派上用场。

function drawRotatedIcon(ctx, x, y, angle, scale) {
  ctx.save();                     // 保存调用函数前的所有状态
  ctx.translate(x, y);            // 将原点移动到图标中心
  ctx.rotate(angle);
  ctx.scale(scale, scale);
  ctx.fillStyle = '#333';
  ctx.fillRect(-10, -10, 20, 20); // 以新原点为中心绘制图标
  ctx.restore();                  // 恢复所有状态,后续绘制不受影响
}

使用这种模式时需要注意以下几点:

  • 成对出现:一组 save() 和 restore() 最好在同一个函数作用域内成对出现,如同给代码块加上一对括号,确保状态隔离。
  • 避免滥用:不要为单条绘图指令包裹一层状态保存(如 ctx.save(); ctx.fillRect(...); ctx.restore();),这只会增加额外开销而无实际意义。
  • 考虑替代方案:若仅需重置变换矩阵,使用 ctx.setTransform(1, 0, 0, 1, 0, 0) 直接重置为单位矩阵,性能开销通常比调用 restore()(会恢复所有状态)更小。

容易被忽略的裁剪路径陷阱

clip() 方法设置的裁剪区域属于绘图状态的一部分,会被 save() 和 restore() 管理,但它的行为有些特殊,容易令人困惑:

  • 可恢复性:调用 clip() 后再执行 save(),裁剪区域会被保存;随后调用 restore() 时,裁剪区域也会被还原或清除。
  • 路径依赖:clip() 作用于当前路径。若在调用 clip() 前未使用 beginPath() 开启新路径,则旧路径(可能来自之前绘图操作)也会一并用于裁剪,导致裁剪范围与预期不符。
  • 非叠加性:多次调用 clip() 并不会使裁剪区域叠加(取交集),而是用最新路径完全替换当前的裁剪区域。

一个安全的裁剪使用示例如下:

ctx.save();
ctx.beginPath();                    // 关键:开启新路径
ctx.rect(50, 50, 100, 100);
ctx.clip();                         // 设置裁剪区域为该矩形
// 此后所有绘制,仅落在 (50,50,100,100) 矩形区域内的部分可见
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 200, 200);       // 实际只有右下角 50x50 区域显示红色
ctx.restore();                      // 恢复状态,裁剪区域失效,后续绘制恢复正常

真正复杂的情况往往发生在裁剪、变换和样式混合使用时。此时 save() 的调用时机至关重要——它决定了希望“冻结”状态的哪一层。许多初学者在此踩坑,便是在设置 clip() 之前忘了 save(),导致在 restore() 之后,裁剪限制依然生效,影响后续所有绘制。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章