# Canvas
# Canvas 面试总览:位图画布、状态机与工程边界
⚡ 30 秒速记
Canvas是即时模式的位图画布:浏览器只保留像素,不保留圆、线、文本这些可查询的图形对象- 一次绘制 =「设置状态 → 建路径 →
fill()/stroke()落笔」;save()/restore()管的是画笔状态栈,不是像素快照 - 高清屏模糊是位图像素不够:位图尺寸乘
devicePixelRatio,CSS尺寸不变,坐标系scale回来 - 动画每帧「更新状态 → 清屏 → 重绘」,用
requestAnimationFrame驱动,离开页面要停 - 大量动态图元、像素处理选
Canvas;要节点语义、无障碍、逐元素事件选SVG/DOM
Canvas 就是一块像素画布,你用 JS 往上画,画完浏览器只记得像素,不记得你画的是圆还是线。 所以它的工作方式是「状态 + 路径 + 落笔」:先设 fillStyle、变换这些画笔状态,用 beginPath()、arc() 建路径,最后 fill() 或 stroke() 才真正画上去。想改某个图形没法「选中它」,只能改 JS 里的数据再整块重画。高清屏发糊要把位图尺寸乘上 devicePixelRatio。图元多、变化快就选 Canvas,要逐个点击、读屏、键盘访问就选 SVG 或 DOM。
CanvasRenderingContext2D 可以看成一台带状态的画笔。fillStyle、strokeStyle、变换矩阵、裁剪区等属性会影响之后的命令;beginPath() 只会清空当前路径,不会擦除画布像素,真正落笔要调用 fill() 或 stroke()。这也是常见 bug 的来源:忘记 beginPath() 会把旧子路径再次描边,忘记 restore() 会让缩放、透明度继续污染后续绘制。
工程里通常把「数据状态」作为事实来源,而不是试图从像素里还原对象。以拖拽图形为例,需要在 JavaScript 中保存每个图形的位置和命中区域,指针移动时自己做 hit test,然后清屏并按新状态完整重绘。复杂场景可以把静态背景画到离屏画布,只重绘变化层;像素计算可交给 Web Worker 或 OffscreenCanvas,但主线程上的 DOM 事件和最终合成仍要纳入性能预算。
高清适配必须区分两种尺寸:canvas.width/height 决定位图缓冲区,style.width/height 决定页面显示大小。只写 CSS 尺寸会把较少的物理像素拉伸;只放大位图又不缩放坐标系,会让逻辑尺寸改变。上面的实验会真实读取当前设备的 devicePixelRatio、重建 backing store,并从绘制结果采样像素,能直观看出两条路径的差别。
💬 面试官追问
-
连画两条线,第二次
stroke()把第一条又描了一遍,为什么?stroke()不会清空路径,第二条线是接在旧路径后面的,描边时一起画了。每画一个独立图形前先调ctx.beginPath(),它只清路径,不会擦掉已经画上去的像素。 -
拖拽流程图节点时,怎么知道鼠标点中了哪个?
画布上没有节点对象,得自己做命中检测:节点数据存在数组里,
pointerdown时把鼠标坐标换算成画布坐标,倒序遍历判断落在哪个节点里,复杂形状可以用ctx.isPointInPath(path, x, y)。命中后改数据,再整块重画。 -
画完一个旋转的元素后,后面所有组件都歪了,怎么回事?
变换没还原。
ctx上的rotate、globalAlpha、clip会一直生效到你改回去为止。局部绘制用ctx.save()和ctx.restore()包起来,中途return的分支也别漏了restore。 -
大屏底图很复杂,只有几个数字每秒在变,整屏重画太卡,怎么优化?
分层。静态底图提前画到离屏
canvas上,每帧drawImage贴过来再画动态部分;或者叠两个canvas,底下那层只画一次。像素计算重的话可以挪进Worker用OffscreenCanvas。 -
签名板在
2x屏要清晰,导出的图又要固定300×150,怎么做?显示时位图按
devicePixelRatio放大,再用ctx.scale(dpr, dpr)把坐标系缩回逻辑尺寸。导出时新建一个300×150的canvas,把高清画布drawImage缩进去再toDataURL(),直接导出原画布会得到600×300的图。