跳到主要内容

保留模式与文档树

SVG 的每个可见图元都是 DOM 节点。改属性或样式,渲染器按树重绘。这和 Canvas 2D 的立即模式相反:Canvas 调用返回后只剩像素,SVG 调用返回后节点还在。

1. 两种模式

保留模式(SVG / DOM)
scene graph ──► 渲染器按树重绘
改 node.cx.baseVal,下一帧自动反映

立即模式(Canvas 2D)
你的代码 ──► 像素
改「逻辑 x」如果自己不重绘,屏幕上什么都不变

生产后果:

  • 拖拽一个矩形,改的是 x / transform,不必自己清屏。
  • 撤销可以是「把属性设回去」或「把节点插回原位」,不必回放绘制命令。
  • 点选可以走 elementFromPoint,也可以对几何做 isPointInFill。两者 不是 同一回事,见 指针事件

2. 最小文档模型

下面这个结构是绝大多数矢量编辑器 / 流程图的底座。它不依赖框架。

const SVG_NS = "http://www.w3.org/2000/svg";

interface SceneNode {
id: string;
el: SVGGraphicsElement;
/** 相对父节点的局部变换;渲染时写到 transform 或 CSS */
local: DOMMatrix;
children: SceneNode[];
pickable?: boolean;
}

export function createRect(init: {
id: string;
x: number;
y: number;
width: number;
height: number;
fill?: string;
}): SceneNode {
const el = document.createElementNS(SVG_NS, "rect");
el.id = init.id;
el.setAttribute("x", String(init.x));
el.setAttribute("y", String(init.y));
el.setAttribute("width", String(init.width));
el.setAttribute("height", String(init.height));
el.setAttribute("fill", init.fill ?? "#3b5bfd");
return {
id: init.id,
el,
local: new DOMMatrix(),
children: [],
pickable: true,
};
}

export function mount(parent: SVGElement, node: SceneNode): void {
parent.append(node.el);
for (const child of node.children) {
mount(node.el, child);
}
}

export function applyLocal(node: SceneNode): void {
const { a, b, c, d, e, f } = node.local;
node.el.setAttribute("transform", `matrix(${a} ${b} ${c} ${d} ${e} ${f})`);
}

关键点:

  • 几何活在元素上,不要另存一份平行坐标数组还指望 DOM 自己对齐。场景对象可以持有元素引用,但写入必须有单一方向。
  • 变换活在矩阵里,再投影到 transform 属性或 CSS transform。不要混用两套还不统一 transform-box
  • 创建必须走 createElementNSdocument.createElement("rect") 在 HTML 文档里不是 SVGRectElement

3. 什么时候可以不自建场景对象

适用:静态图标、一次渲染的图表、CSS 就能完成的 hover。这时 DOM 树本身就是场景图

不适用:对象要成组、对齐、命令式撤销、按业务 ID 查询。没有自己的 SceneNode 你会把状态拆进 dataset、闭包和 CSS 类,然后无法做父子变换的逆运算。

4. 失败形态

症状原因
拖一下节点「粘」在错误位置改了 CSS 像素,没过 getScreenCTM 逆变换
新建的 circle 不显示createElement 没用 NS,元素不在 SVG 命名空间
撤销后 DOM 对、画面不对改了内存模型没写回 attribute / CSS
点到空白却选中组<g>pointer-events 把子节点包成一块命中区

权威资料

核对日期:2026-08-26