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

  1. 在初始视口记录 workingSetSizerenderedObjectCount
  2. 移到远区,观察 exit 先进入 grace,再产生 release;
  3. grace 内快速返回,确认 handle 被复用;
  4. 等待 grace 后返回,确认重新 hydrate;
  5. 删除一个可见节点,确认不等待 grace 且 History 增加;
  6. 对比隐藏对象和真实 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 和事件绑定的处理顺序。