状态栈与当前路径
save / restore 管理的是 drawing state 栈。WHATWG 明确:current default path 不是 drawing state。这是 Canvas 2D 里被踩得最多的坑。
1. 栈里有什么
实现必须保存(不完整枚举,以规范为准):
- 当前变换矩阵(CTM)
- 裁剪区
strokeStyle/fillStyle/globalAlpha/globalCompositeOperation- 线型:
lineWidth、lineCap、lineJoin、miterLimit、lineDash、lineDashOffset - 阴影、
filter、imageSmoothingEnabled、imageSmoothingQuality - 文本:
font、textAlign、textBaseline、direction、fontKerning等 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 才能解除。漏 restore 的 clip 会让后续所有绘制「消失」。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