Working Set 是上架清单,Renderer 是真实展台。Hydrate 把节点摆上展台,Dehydrate 只撤下展台,不删除目录与仓库。
学习目标
- 区分 Document、AssetStore、Working Set 和 Renderer;
- 描述图片节点从导入到离屏再重访的完整生命周期;
- 正确处理异步 hydrate、取消、重入和 exit grace;
- 判断
visible(false)为什么不是资源释放; - 用计数和资源状态验收生命周期。
1. 四层分别回答四个问题
| 层 | 回答的问题 | 典型数据 |
|---|---|---|
| Document | 作品里有什么 | id、kind、World Bounds、assetId |
| AssetStore | 素材在哪里 | original/preview/thumbnail Blob |
| Working Set | 当前需要谁 | Visible、Overscan、Pinned ID |
| Renderer | 谁已真实上架 | Konva Node、DOM video、ImageBitmap |
Working Set 是计划,Renderer Registry 是当前事实。节点刚进入时可能还在异步解码,所以两者短暂不相等是正常的;稳定后应该收敛。
2. Hydrate 到底做了什么
读取 Document Record
→ 解析 assetId 和当前 LOD
→ 获取资源 lease
→ 异步读取/解码素材
→ 创建 Renderer handle
→ 绑定必要事件
→ 注册 Map<nodeId, handle>
→ 请求 draw
不同节点的 hydrate 成本不同:矩形可能只创建一个轻量 handle;图片需要读取和解码;视频通常先创建 poster,真正播放时才创建播放器。
Hydrate 必须幂等:同一个节点已存在 handle 时,应 update/reuse,而不是重复创建第二个对象。
3. Dehydrate 与 Delete 完全不同
dehydrate(nodeId)
取消加载、解绑事件、清 cache、销毁 handle
释放 lease、Object URL、播放器或 bitmap 引用
保留 Document、Spatial Entry、Asset Reference、History
delete(nodeId)
提交作品事务
删除 Document 与 Spatial Entry
销毁当前 handle
记录可 Undo 的 before/after
只调用 visible(false) 可能减少绘制,却仍然保留 Konva Node、事件、图片解码对象、cache 和 GPU texture。它属于隐藏,不是 Dehydrate。
4. 一张图片的完整生命周期
导入
→ AssetStore 保存 original/variants
→ Document 新增 node-A,只引用 assetId
Camera 靠近
→ SpatialIndex 返回 node-A
→ node-A 进入 Working Set
→ hydrate(node-A)
→ 选择 LOD、解码、创建 handle
Camera 离开
→ node-A 进入 exit grace
→ 仍未返回则 dehydrate(node-A)
→ Document 与 Blob 继续保留
Camera 重访
→ 根据同一 Document + AssetStore 再次 hydrate
重访后的位置和内容来自 Document,不来自上一次残留的 Renderer 对象。
5. 异步竞态是生命周期的难点
典型竞态:节点进入 Working Set 后开始解码,但解码完成前 Camera 已经移走。
const generation = registry.begin(node.id)
const resource = await pool.acquire(key, loader)
if (!registry.isCurrent(node.id, generation) || !workingSet.has(node.id)) {
resource.release()
return
}
renderer.hydrate(node, resource.value)
每次 hydrate 应带 generation 或 AbortSignal。迟到的 Promise 不能把已退出节点重新插回 Renderer,也不能泄漏 lease。
删除比普通 exit 优先级更高:不等待 grace,立即取消加载并销毁 handle。
6. 为什么需要 exit grace
视口边界处可能发生:
exit → enter → exit → enter
若每次都立即销毁和解码,会造成抖动。exit grace 的策略:
节点退出
→ 记录 deadline
→ grace 内重新进入:取消卸载,复用 handle
→ deadline 后仍未进入:dehydrate
grace 不是越大越好。过大会让快速平移后大量旧对象长期驻留。它必须与最大 handle 数、资源预算和设备能力一起调节。
7. 视频节点要释放到什么程度
常见结构是 Canvas/Konva 显示 poster,只有激活时创建 DOM <video>。退出时:
video.pause()
video.removeAttribute('src')
video.load()
video.remove()
只 pause() 不一定释放网络缓冲和解码器。正在播放的视频通常属于 Pinned,离开 Visible 后何时暂停由宿主策略决定,但播放器总数仍应受预算控制。
8. Renderer Adapter 的职责
以 Konva 为例:
hydrate → new Group/Image/Text,绑定事件
update → 同步 Document 属性和已选资源
dehydrate → off、clearCache、解除 image、destroy
destroy → 清空整个 Registry 和 Stage
Canvas2D 没有逐节点场景对象,但仍可能有图片解码资源、命中缓存或 DOM overlay,需要同样的生命周期管理。
9. 操作 Demo
- 在初始视口记录
workingSetSize和renderedObjectCount; - 移到远区,观察 exit 先进入 grace,再产生 release;
- grace 内快速返回,确认 handle 被复用;
- 等待 grace 后返回,确认重新 hydrate;
- 删除一个可见节点,确认不等待 grace 且 History 增加;
- 对比隐藏对象和真实 destroy 后的对象/资源计数。
10. 验收标准
稳定后 renderedObjectCount ≈ workingSetSize
离屏后 documentNodeCount 不变
离屏并超过 grace 后 handle/lease 数下降
迟到的异步加载不会重新挂载退出节点
重访后节点位置、内容、层级和历史不丢失
delete 可 Undo,dehydrate 不产生作品历史
destroy Renderer 后运行时对象归零
常见错误
visible(false)冒充 Dehydrate;- exit 后没有取消异步加载;
- 同一节点重复 hydrate 多个 handle;
- 销毁节点却忘记 release 共享资源 lease;
- 把 grace 设得很长却没有总对象预算;
- 把离屏当删除,导致保存和 Undo 丢数据。
思考题
Working Set 已删除 node-A,但它的 hydrate Promise 随后成功。除了不创建 handle,还必须释放哪些临时对象?请列出 lease、ImageBitmap、Object URL 和事件绑定的处理顺序。