跳到主要内容

Group 分组

Konva.Group 把若干节点装进一个容器,之后对容器做的变换会作用到全部子节点。 拖一次就带走一整组、旋转一次整体转——不用给每个图形各自算坐标。

const group = new Konva.Group({
x: 20,
y: 20,
rotation: 12,
draggable: true,
});

group.add(rect);
group.add(circle);
group.add(text);
layer.add(group);

子节点的 x / y 是相对于 Group 的,不是舞台坐标。 Group 移动时子节点的 x / y 不变,变的是 Group 自己。

Group 与 Layer 的区别​

两者都是容器,都能 add() 子节点,但底层完全不同:

LayerGroup
对应 DOM一个真实的 <canvas> 元素无
内存开销一张场景画布 + 一张命中画布几乎为零
独立重绘可以,其他层不受影响不能,随所在图层一起重绘
数量建议少,超过 5 个 Konva 会打印告警不限

判断标准很清楚:要「这部分变化时别的不用重画」用 Layer, 要「这几个东西一起动」用 Group。

新手常见的错误是为了组织结构而拆很多 Layer。 每个 Layer 都要在 DOM 里放一个画布元素,在高分屏上一张全屏画布 就是几千万像素的显存,拆到十几层会明显拖慢合成。

Konva 自己会拦你——stage.add() 时图层数超过 5 就打印一条告警, 原文是:

The stage has N layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.

纯粹的逻辑分组一律用 Group。

Group 没有自己的尺寸​

Konva.Group 不是 Shape,它没有 sceneFunc, 因此没有 fill、stroke,也没有真正的宽高:

group.width(); // 0 —— 不管里面装了多大的东西
group.height(); // 0

想知道这一组占多大地方,用 getClientRect():

const box = group.getClientRect();
// { x, y, width, height } —— 含子节点、变换与描边的实际包围盒

这也是给 Group 加背景、加边框的办法:算出包围盒, 再往 Group 里插一个 Konva.Rect 放在最底层。注意插入之后包围盒会变, 要先算再插,不要在循环里反复取。

常见问题​

变换 Group 之后子节点的坐标变了吗?​

没有。子节点的 x() / y() 始终是相对父容器的值, Group 的变换矩阵在绘制时才累乘上去。

要拿舞台坐标系里的真实位置,用 getAbsolutePosition():

circle.x(); // 30 —— 相对 Group
circle.getAbsolutePosition(); // { x: ..., y: ... } —— 相对舞台

这一点在做碰撞检测、吸附、对齐时特别容易出错—— 两个不同 Group 里的节点,直接比较 x() 没有意义。

能给 Group 设 fill 吗?​

不能。Group 是容器,没有可填充的路径。

要给一组内容加背景,在 Group 里放一个 Konva.Rect 作为第一个子节点:

const box = group.getClientRect({ relativeTo: group });
const bg = new Konva.Rect({
x: box.x,
y: box.y,
width: box.width,
height: box.height,
fill: '#f5f5f5',
});
group.add(bg);
bg.moveToBottom();

{ relativeTo: group } 不能省——默认返回的是舞台坐标系里的包围盒, 直接拿去当子节点的 x / y,会额外偏移一个 Group 自身的位置。

怎么让 Group 里只有某几个子节点响应事件?​

给不需要响应的子节点设 listening(false)。 注意 Group 自己设 listening(false) 会让整组都不响应, 包括所有子节点——这是命中检测里 最常见的困惑来源。

反过来,Group 设了 draggable: true 之后, 点在任意子节点上都能拖动整组; 如果想让某个子节点单独可拖,给它自己也设 draggable: true, 它的拖拽会覆盖父级的。

Group 可以嵌套吗?​

可以,而且没有层数限制。但每多一层嵌套, 每个子节点在绘制和求绝对坐标时就多一次矩阵乘法。

深层嵌套在节点多的时候会明显拖慢。 如果你发现自己嵌了五六层,通常说明结构可以拍扁—— Group 的层级应该反映一起变换的单元,而不是业务上的分类。

性能提示​

Group 本身没有渲染开销——它不绘制任何东西,只是传递变换。 所以「为了组织结构而分组」是免费的,可以放心用。

真正影响性能的是围绕 Group 的两个操作。

group.cache() 是性价比很高的优化,也很容易用错。 它把整组内容烤成一张位图,之后绘制这一组就等于画一张图, 成本与内部有多少节点无关。一个装了两百个小图形的静态 Group, 缓存后绘制耗时能降一两个数量级。

代价是缓存之后子节点的改动不会自动反映—— 改了子节点的属性,画面纹丝不动,必须 group.clearCache() 或重新 group.cache()。这是缓存最常见的坑: 「我明明改了值,为什么没变化」。

所以判断标准是这一组会不会频繁变: 静态或很少变的,缓存收益巨大;每帧都在改内部的,缓存是净损失 (每次都要重建位图)。

getClientRect() 不便宜。 它要递归遍历所有子节点、 逐个求变换后的包围盒再取并集。不要在 dragmove 之类的高频回调里 对一个大 Group 反复调用——把结果缓存起来, 只在内容真正变化时重算。

Konva 10.4.0 起 clearCache() 的行为有过修正: 此前对容器调用会把所有后代的缓存也一起清掉并泄漏它们的画布, 现在不会了。如果你的项目还在更早的版本上大量使用嵌套缓存, 这是一个值得升级的理由。