render-buckets
本文回答:vjmapext 渲染桶(hot / cold / dynamic)原理 + 实体如何进哪个桶。
一、什么是渲染桶
MapCadLayer 内部把实体按状态分成 3 个桶——每个桶是一条独立的 GeoJSON source(mapcad-hot / mapcad-cold / mapcad-dynamic),样式表按 _active 属性区分高亮。feature properties 里的实体回链键是 _entity_id(另有 _entity_type/_role/_meta),调试用 map.getSource('mapcad-cold')._data.features 直读比 querySourceFeatures(走瓦片管线,异步生成)更稳:
| 桶 | 何时进 | 特点 |
|---|---|---|
| cold(冷) | 普通实体:未选中、未高亮、非 dynamic | 绝大多数实体在这;桶脏时整桶 setData 重传 |
| hot(热) | 被选中(isSelected)或高亮(isHighlighted)的实体 | 桶很小,选中态单独走 _active 高亮样式 |
| dynamic(动态) | 设了 entity.dynamic = true(且未选中) |
专为高频变化实体准备的小桶 |
注意:三桶都是 GeoJSON source,没有
DYNAMIC_DRAW级区别(DYNAMIC_DRAW是 FX instancing 的内部细节);dynamic的收益是"小桶重传便宜",不是特殊 buffer 类型。
二、流转规则
每次增量更新(_incrementalUpdate)都按同一条规则重新分类:
store.isSelected(id) || store.isHighlighted(id) → hot
否则 entity.dynamic === true → dyn
否则 → cold
addEntity→ 默认进 cold(除非一进来就被选中 / dynamic)- 选中实体 → 升入 hot;取消选中 → 降回 cold / dyn
changed()的实体 → 从原桶摘除、重新编码、按上表重新分桶;所在桶标脏 → 下一帧该桶setData重传styleChanged()走updateStyleProps快路径:只重算样式属性,不重编码几何(桶照样标脏重传)
没有"hot 静置 N 秒后自动降级"机制——hot 完全由选中/高亮状态决定,取消选中即离开。
三、什么时候用 dynamic
当实体每帧都要变位置 / 大小(比如飞线、闪烁、路径跟随):
const poly = new vjmapext.PolylineEnt(pts);
poly.dynamic = true;
mapcadLayer.addEntity(poly);
setInterval(() => {
const vs = newPoints();
poly.bulgePoints = vs.map(([x, y]) => ({ point: new vjmapext.Point2D(x, y), bulge: 0 }));
poly.changed(); // 高效:只改 dynamic buffer
}, 16);
如果不设 dynamic,cold 桶每次都会整桶 rebuild,1 万实体下一帧卡死。
四、hot 桶 = 选中/高亮桶
isSelected || isHighlighted 为 true 的实体进 hot 桶,feature 打上 _active = "true",由样式表里的 _active 过滤器渲染成高亮样式。
- 用户 click 选中 → 实体升入 hot,立刻显示选中态
- 取消选中 /
store.setSelection([])→ 降回 cold(或 dyn) - hot 桶始终很小 → 选中态变化的重传代价极低
选中几百上千实体时 hot 桶同样会变大——超大选集的高亮渲染成本与选集大小成正比。
五、查看当前桶分布(调试)
renderPipeline 无公开的桶统计 API;可在浏览器 devtools 里查看实例的 _hotFeatures / _coldFeatures / _dynFeatures 集合大小(私有字段,仅供调试,勿依赖)。
如果 hot 一直很大(上千)→ 意味着有"隐形"反复变化的实体,需排查。
六、性能对比(测试数字仅供参考)
| 场景 | cold 桶 | hot 桶 | dynamic 桶 |
|---|---|---|---|
| 1 万实体静态显示 | 60 FPS | - | - |
| 加 1 条新实体 | 60 → 59 FPS(rebuild 一次) | 60 FPS | 60 FPS |
| 每帧改 100 条颜色 | 30 FPS(每帧 rebuild) | 50 FPS | 60 FPS |
| 每帧改 100 条几何 | 5 FPS | 20 FPS | 58 FPS |
七、什么时候不要设 dynamic
- 实体只是偶尔变(比如用户点一次改一次颜色)→ 默认就好
- 实体数 < 100 且都会动 → 默认 cold 也能 60 FPS
八、cold 桶批量加的最优做法
// 正确:直接循环——addEntity 只标脏,重绘按 requestAnimationFrame 合并成一帧
for (const p of data) mapcadLayer.addEntity(toEnt(p));
// 注意:不要在循环里穿插 await / 异步,会把添加摊到多个帧
详见 batch-rendering 和 batch/entity-bulk-add。
九、常见坑
- 期望实体高频变化但没设 dynamic → 每帧 rebuild 整个 cold buffer,几千实体下 FPS 断崖。
- 所有实体都设 dynamic → dynamic 桶膨胀,每帧都要走完整编码路径,反而不如 cold。
- 忘记
deleteEntity的动态实体:实体一直在 store 和 dyn 桶里,每帧随 dyn sourcesetData上传;不存在"slot 溢出",纯粹是白白吃内存和带宽。 - 标脏后画面不更新:确认确实调了
changed()/styleChanged();只改字段不标脏不会触发重绘。 - dynamic + styleChanged:同样有效且更省——
updateStyleProps原地改缓存 feature 的样式属性,连toGeoJSON重编码都跳过;只改样式时优先用它而不是changed()。 - 一个"毒实体"拖死整条管线:store 里任一实体的点字段被赋
{x,y}/数组字面量(而非Point2D实例)时,_fullRebuild→renderCache.getOrCompute→toGeoJSON/clone抛clone is not a function,整帧渲染中断——三个桶全部清空,且页面无业务层报错(异常发生在 rAF 回调里,仅见 pageerror)。更糟的是store.delete(id)也会崩:undoManager.recordDelete→entity.clone()同样抛错,毒实体连正常删除都走不通。应急清除:先store._undoManager = null绕过 undo 快照再store.delete(id)(删完恢复引用),或定位后整体fromJSON重建。 - 挂载时机:
addControl要么在 map 'load' 之前,要么在map.loaded()为真时(官方示例是await map.onLoad()后紧接着挂,这两种都安全)。onLoad 之后又等了别的异步、地图正加载新瓦片时才挂,loaded()为 false 而 'load' 不再发 → onAdd 的loaded()?doConnect():on('load',doConnect)两路落空 → 桶永远不建、画面静默空。补救:map.fire('load')重发事件。另注意removeControl会removeSourcesAndLayers拆掉共享的 mapcad-* 桶——同一 map 挂第二个实例再移除,会把第一个的桶也清掉且不重建。
相关
- dynamic 详细 → dynamic-entities
- 样式快路径 → style-only-fast-path
- 批量 → batch-rendering