浅谈 Canvas 渲染引擎设计( 四 )


基于 Group 来做离屏渲染的原理是:

  1. 调用 cache 方法 , 创建一个离屏 Canvas 节点 。
  2. 遍历 Group 子节点进行绘制 , 同时将其绘制到离屏 Canvas 上面 。
  3. 下次 batchDraw 的时候判断是否有缓存 , 如果有 , 那么直接走 drawImage 的形式 。
这种离屏渲染的调用方式比较简单 , Group 的粒度可以由开发者自己决定 , 但也有一定的问题 。
  1. 比较难应用于表格这种形式的业务
  2. Konva 没有脏检测能力 , 即使 Group 里面的 Shape 属性改变了 , 依然不会更新离屏 Canvas 。
  3. 由于使用色值法来匹配图形 , 导致开启了离屏渲染 , 实际上至少要绘制四份(主canvas、事件 hitCanvas、离屏 cacheCanvas、离屏事件 cacheHitCanvas) 。
为什么需要绘制四份呢?因为离屏渲染是 drawImage 的形式 , 这样就不会有 colorKey 和 Shape 对应的情况了 , 所以离屏 Canvas 也要有一个自己的 hitCanvas 来做 getImageData , 也就是 cacheHitCanvas 。
另一种场景的离屏渲染就是飞书 Bitable 里面的实现 。
飞书在底层之上封装了虚拟列表的 Widget , 也就是基于业务定制的 Widget , 这也是一种有趣的思路 。
  1. 创建一个虚拟列表的 Widget 类 , 将列表数据传入
  2. 实现列表每一项的绘制方法 , 将列表绘制出来
  3. 滚动的时候虚拟列表内部进行节点的回收创建 , 但不会进行异步批量渲染 , 针对可复用的部分进行离屏渲染
  4. 更新阶段 , 通过 key 对比来决定是回收、创建还是复用 。
在多维表格看板视图里面 , 每个分组都是一个虚拟列表 , 多个分组(虚拟列表)又组合成一个大的虚拟列表 。
多选单元格编辑器也可以基于虚拟列表实现 。
虚拟列表 Widget 类适合多维表格这种业务 , 多个视图都需要有自己的滚动容器 , 不同视图都需要处理节点的回收、复用、新建 , 通过公用 Widget 可以一步到位去支持 , 也方便在内部去做更多性能优化 。
4.3 脏区渲染对于 Konva 来说 , 每次重新渲染都是对整个 Canvas 做 clearRect 清除 , 然后重新绘制 , 性能相对比较差 。
更好的做法是检测到当前的改动影响到的范围 , 计算出重绘范围后 , 只清除重绘区的内容重新进行绘制 。
在 Canvas 中可以通过 rect 和 clip 限制绘制区域 , 从而做到只对部分区域重绘 。
以前 ECharts 底层的 ZRender 为例来讲解:
  1. 根据图形前后变化 , 来计算出重绘区域 , 比如上图的区域 , 在飞书文档中会将整个移动的路径当做重绘区域 。
  2. 如果有多个重绘区域 , 那么优先尝试将相交(包围盒)的重绘区进行合并 , 并且优先合并相交面积最大的重绘区 。
  3. 如果合并完成后 , 当前剩余的重绘区数量大于5 , 则进一步进行合并 , 直到数量只剩5 。
  4. 依次遍历这些重绘区域 , 先清除掉原有的内容 , 再进行绘制 。
飞书文档多维表格没有做 Canvas 渲染分层 , 但对各种交互响应速度非常快 , 也是得益于底层渲染引擎对脏矩形渲染的支持 , 它的性能也是所有同类产品里面最好的 。
除了上述的这些 , 还有在文档这边使用的一些优化手段 , 比如合并相同属性的图形绘制(线、矩形、文本等)、Canvas 分层等等 , 这些就不多做阐述了 。
5. 跨平台很多 Canvas 渲染引擎并不满足于只做 Canvas , 一般还会支持一些其他的渲染模式 , 比如 SVG 渲染、WebGL 渲染、WebGPU 渲染等等 。
在 AntV 里面通过引入对应的 package 来实现加载渲染器的 , 在 ZRender 中则是通过 register 来注册不同的渲染器 。
AntV 中使用 CanvasKit 渲染:


推荐阅读