跳到主要内容

状态栈与当前路径

save / restore 管理的是 drawing state 栈。WHATWG 明确:current default path 不是 drawing state。这是 Canvas 2D 里被踩得最多的坑。

1. 栈里有什么

实现必须保存(不完整枚举,以规范为准):

  • 当前变换矩阵(CTM)
  • 裁剪区
  • strokeStyle / fillStyle / globalAlpha / globalCompositeOperation
  • 线型:lineWidthlineCaplineJoinmiterLimitlineDashlineDashOffset
  • 阴影、filterimageSmoothingEnabledimageSmoothingQuality
  • 文本:fonttextAligntextBaselinedirectionfontKerning
  • shadow*、部分新字段(如 letterSpacing)随引擎增加

不在栈里:

  • backing store 像素
  • 当前默认路径
  • bitmap 本身

因此:

const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2d context missing");

ctx.beginPath();
ctx.rect(10, 10, 80, 80);
ctx.save();
ctx.beginPath();
ctx.rect(40, 40, 80, 80);
ctx.restore();
ctx.fillStyle = "red";
ctx.fill(); // 填的是 restore 之后仍然存在的「当前路径」= 第二个 rect

restore 把样式和 CTM 弹回去,不会把路径弹回第一个 rect。路径只能 beginPath() 丢掉,或改用 Path2D 彻底离开「当前路径」这个隐式全局变量。

2. 生产约定:Path2D + withState

不要让业务代码碰当前默认路径。把几何关进 Path2D,把样式关进一次 save/restore

export function withState(
ctx: CanvasRenderingContext2D,
paint: (ctx: CanvasRenderingContext2D) => void,
): void {
ctx.save();
try {
paint(ctx);
} finally {
ctx.restore();
}
}

export function fillPath(
ctx: CanvasRenderingContext2D,
path: Path2D,
style: string | CanvasGradient | CanvasPattern,
): void {
withState(ctx, (c) => {
c.fillStyle = style;
c.fill(path);
});
}

clip() 会把路径交进裁剪区,且裁剪区只能缩小不能扩大,必须靠 restore 才能解除。漏 restoreclip 会让后续所有绘制「消失」。withState 就是为这个写的。

3. reset()resetTransform()

  • resetTransform():只把 CTM 设为单位阵,样式和 clip 还在。
  • ctx.reset()(较新):清空 backing store、状态栈、路径、样式,接近「新 context」。不要在帧中间当 clearRect 用。

栈不平衡(save 多于 restore)会让变换/clip 泄漏到下一帧。调试时可以在每帧开头 setTransform(1,0,0,1,0,0) 并避免跨帧悬挂的 save

4. 失败形态

症状原因
restore 后填充形状变了路径不入栈,当前 path 仍是后来的
画面越画越小/只剩一角clip 未 restore
某一层透明度污染全局globalAlpha 未 restore
文字突然变成上一层的 font状态泄漏

权威资料

核对日期:2026-08-26