背景

ZRender 是 Echarts 的底层图形绘制引擎,它是一个独立发布的基于 Canvas/SVG/VML 的 2D 图形绘制引擎,提供功能:

  • 图形绘制及管理(CRUD、打组)
  • 图形(包含文字)动画管理
  • 图形(包含文字)事件管理(Canvas 中实现 dom 事件)
  • 基于“响应式”(dirty 标记)的高效帧渲染机制
  • 可选择的渲染器机制:Canvas/SVG/VML(5.0 已放弃 VML 支持)

PS:图形特指 2D 矢量图型

整体架构

基于MVC模式的整体架构

在这里插入图片描述

如图所示,ZRender 的整体设计思路是面向对象的 MVC 模式,视图层负责渲染,控制层负责用户输入交互,数据层负责数据模型的编排与存储,其对应的文件和作用如下:

  • Storage.ts(数据模型):用于存储所有需要绘制的图形数据,并且提供相关数据的 LRU 缓存机制,提供数据的 CURD 管理
  • PainterBase.ts(视图绘制):PainterBase 是绘制的基类,系统提供的 Canvas、SVG、VML 视图绘制类都继承于 PainterBase 类,用户也可以自行继承实现如 webgl 的绘制能力
  • Handler.ts(交互控制):事件交互控制模块,为图形元素实现和 HTML、DOM、Element 一样的事件交互逻辑,如图形的选中、单击、触摸等事件

辅助功能模块

除了上述 MVC3 大模块以外,还有以下辅助功能模块:

  • 动画管理模块(animation):管理图形的动画,绘制前会将对象的动画计算成帧对象保存在动画管理器中,伴随着动画触发条件,将帧数据推送给视图绘制模块进行动画绘制
  • 工具类模块(tool、core):提供颜色转换、路径转换、变换矩阵运算、基础事件对象封装、向量计算、基础数据结构等独立辅助计算函数或者类
  • 图形对象模块(graphic):提供元素的对象类(包含 Image、Group、Arc、Rect 等),所有元素其最顶层都继承于 Element 类
  • 图形对象辅助模块(contain):提供用于判断包含关系的算法,比如:坐标点是否在线上,坐标点是否在图形内

PS:上文中“元素”包含 Group 和 2D 图形,而图形只包含 2D 图形,不包含 Group

源码文件结构

目录结构

src/
  -|config.ts
  -|Element.ts
  -|Storage.ts
  -|Handler.ts
  -|PainterBase.ts
  -|zrender.ts //入口文件
  -|export.ts
  -|animation/
    -|Animation.ts
    -|Animator.ts
    -|Clip.ts
    ...
  -|canvas/
    -|Painter.ts
    ...
  -|svg/
    -|Painter.ts
    ...
  -|vml/
    -|Painter.ts
    ...
  -|conatin/
    -|arc.ts
    ...
  -|core/
    -|LRU.ts
    -|matrix.ts
    ...
  -|dom/
  -|graphic/
    -|Group.ts
    -|Image.ts
    -|Path.ts
    -|shape/
      -|Arc.ts
      -|Rect.ts
      -|Circle.ts
      ...
    ...
  -|mixin/
    -|Draggable.ts
  -|tool/
    -|color.ts
    -|ParseSVG.ts
    -|parseXML.ts

目录及文件介绍

文件名 文件介绍
config.ts 全局配置文件,可配置 debug 模式、retina 屏幕高清优化、深/浅主题色值等
Element.ts 所有可绘制图形元素和 Group 的基类,其中定义了基础属性、对象的基础成员方法
Storage.ts M层,对象模型层/存储器层,存储并管理元素对象实例,元素对象实例存储在 _displayableList 数组中,每次绘制时会根据 zlevel -> z -> 插入顺序进行排序,提供添加、删除、清空注销元素对象实例的方法
Handler.ts C层,控制层/器,用于向元素上绑定事件,实现 DOM 式事件管理机制
PainterBase.ts V层,视图层/渲染器层,PainterBase 是渲染器的基类,5.0 版本默认提供 Canvas、SVG 渲染器,5.0 之前版本还提供 VML 渲染器,元素的绘制就是由渲染器决定,系统默认 Canvas 渲染器渲染
zrender.ts ZRender 入口文件,也是编译主入口
export.ts 编译时调用,用于对外导出 API
animation 存放动画相关的代码文件,如:Animation,Animator 等
canvas/svg/vml 存放 canvas/svg/vml 渲染器相关的代码文件
contain 用于补充特殊元素的坐标包含关系计算方法,如贝塞尔曲线上的点包含关系计算
core 工具方法文件,包含 LRU 缓存、包围盒计算、浏览器环境判断、变换矩阵、触摸事件实现等方法
dom 仅 HandlerProxy.ts 一个程序文件,用于实现 DOM 事件代理,所有画布内元素的事件都是从画布 DOM 的事件进行代理进入
graphic 所有元素的实体对象类都存放在这个文件夹,包含 Group、可绘制对象基类 Displayable、路径、圆弧、矩形等
mixin 仅 Draggable.ts 一个文件,用于管理元素的拖拽事件,因为 Echarts 用不上拖拽,所以拖拽事件还没有在 ts 版本中实现
tool 工具方法,提供颜色计算、SVG 路径转换等工具类

入口文件源码分析(zrender.ts)

ZRender全局暴露的方法

zrender.ts 中对外暴露的全局方法如下所示,全局方法可通过 zrender.xxx 即可调用,如:zrender.init()

全局方法主要用于管理 ZRender 实例,如初始化、删除、查找、注销等操作

// 用于存放渲染器
const painterCtors: Dictionary<PainterBaseCtor> = {};

// 用于存放ZRender实例,后文对于实例统称zr
let instances: { [key: number]: ZRender } = {};

/**
 * 按id删除ZRender实例
 */
function delInstance(id: number) {
  // 代码省略
}

/**
 * 初始化ZRender实例,需要传入dom节点作为canvas父级
 */
 export function init(dom: HTMLElement, opts?: ZRenderInitOpt) {
  const zr = new ZRender(zrUtil.guid(), dom, opts);
  instances[zr.id] = zr;
  return zr;
}

/**
* 注销zr实例,注销后会将zr实例内的图形全部删除,不可恢复
*/
export function dispose(zr: ZRender) {
  zr.dispose();
}

/**
* 注销ZRender中管理的所有zr实例
*/
export function disposeAll() {
  // 代码省略
}

/**
* 通过实例id获取zr实例
*/
export function getInstance(id: number): ZRender {
  return instances[id];
}

/**
 * 注册渲染器,系统在启动时会默认注册Canvas和SVG渲染器
 */
export function registerPainter(name: string, Ctor: PainterBaseCtor) {
  painterCtors[name] = Ctor;
}

class ZRender {
  // 后文详解
}

ZRender对象类

ZRender 类写在入口文件 zrender.ts 中

class ZRender {
  // 画布渲染的容器根节点,必须是一个HTML元素
  dom: HTMLElement
  // zr实例id
  id: number
  // 存储器对象实例
  storage: Storage
  // 渲染器对象实例
  painter: PainterBase
  // 控制器对象实例
  handler: Handler
  // 动画管理器对象实例
  animation: Animation

  constructor(id: number, dom: HTMLElement, opts?: ZRenderInitOpt) {
    // 初始化容器根节点
    this.dom = dom;
    // 全局init函数会生成guid传入
    this.id = id;
    // new存储器实例
    const storage = new Storage();
    // 默认渲染器类型为canvas
    let rendererType = opts.renderer || 'canvas';
    // 创建渲染器
    const painter = new painterCtors[rendererType](dom, storage, opts, id);
    // 将存储器实例赋值给成员变量
    this.storage = storage;
    // 将渲染器赋值给成员变量
    this.painter = painter;
    // 创建事件管理器
    this.handler = new Handler(storage, painter, handerProxy, painter.root);
    // 创建动画管理器并启动动画管理程序
    this.animation = new Animation({
      stage: {
        update: () => this._flush(true)
      }
    });
    this.animation.start();
  }

  /**
   * 向画布添加元素,等待下一帧渲染
   */
  add(el: Element) {
    // 代码省略,后续的方法体代码如果无特别说明都会省略
  }

  /**
   * 从存储器中间元素删除,下一帧该元素将不会被渲染
   */
  remove(el: Element) { }

  /**
   * 配置图层顺序、开启动态模糊等
   */
  configLayer(zLevel: number, config: LayerConfig) { }

  /**
   * 设置画布背景色
   */
  setBackgroundColor(backgroundColor: string | GradientObject | PatternObject) { }

  /**
   * 获取画布背景色
   */
  getBackgroundColor() { }

  /**
   * 将zr强制设置为深色模式
   */
  setDarkMode(darkMode: boolean) { }

  /**
   * 查询当前zr是否深色模式
   */
  isDarkMode() { }

  /**
   * 执行强制刷新画布
   */
  refreshImmediately(fromInside?: boolean) { }

  /**
   * 执行下一帧刷新画布
   */
  refresh() { }

  /**
   * 执行所有刷新操作
   */
  flush() {
    this._flush(false);
  }

  /**
   * 设置动画静止帧数,动画将会在设置的帧数后停止执行
   */
  setSleepAfterStill(stillFramesCount: number) {
    this._sleepAfterStill = stillFramesCount;
  }

  /**
   * 唤醒动画,等下次渲染时执行
   */
  wakeUp() { }

  /**
   * 下一帧显示鼠标悬浮状态
   */
  refreshHover() { }

  /**
   * 强制执行鼠标悬浮状态
   */
  refreshHoverImmediately() { }

  /**
   * 调整画布大小
   */
  resize(opts?: {
    width?: number | string
    height?: number | string
  }) { }

  /**
   * 强制停止并清空动画
   */
  clearAnimation() { }

  /**
   * 获取画布宽度
   */
  getWidth(): number { }

  /**
   * 获取画布高度
   */
  getHeight(): number { }

  /**
   * 将路径绘制成图片,提高绘制性能
   */
  pathToImage(e: Path, dpr: number) { }

  /**
   * 设置鼠标样式
   * @param cursorStyle='default' 例如 crosshair
   */
  setCursorStyle(cursorStyle: string) { }

  /**
   * 查找鼠标当前位置元素的对象实例
   */
  findHover(x: number, y: number): {
    target: Displayable
    topTarget: Displayable
  } { }

  /**
   * 挂载全局事件,这里是ts的on方法多态
   */
  on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx, unknown>, context?: Ctx): this
  on<Ctx>(eventName: string, eventHandler: EventCallback<Ctx, unknown>, context?: Ctx): this
  on<Ctx>(eventName: string, eventHandler: EventCallback<Ctx, unknown> | EventCallback<Ctx, unknown, ElementEvent>, context?: Ctx): this { }

  /**
   * 卸载全局事件
   */
  off(eventName?: string, eventHandler?: EventCallback<unknown, unknown> | EventCallback<unknown, unknown, ElementEvent>) { }

  /**
   * 按照事件名称手动触发事件
   */
  trigger(eventName: string, event?: unknown) { }


  /**
   * 清空画布及其已绘制的图形元素
   */
  clear() { }

  /**
   * 将ZRender对象注销
   */
  dispose() { }
}

案例分析ZRender工作流程

案例

下面代码是绘制一个半径为 30px 的玫红色(色值#FF6EBE)圆形,并为圆形绑定左右移动循环动画。

// 1.声明绘制ZRender实例的DOM容器
let container = document.getElementsById('example-container')[0];
// 2.初始化ZRender实例zr、同时zr会绘制画布与container同宽高
let zr = zrender.init(container);

// 3.获取zr画布的宽高
let w = zr.getWidth();
let h = zr.getHeight();

// 4.设定圆的半径为30px
let r = 30;

// 5.创建圆形对象实例cr
let cr = new zrender.Circle({
  shape: {
    cx: r,
    cy: h / 2,
    r: r
  },
  style: {
    fill: 'transparent',
    stroke: '#FF6EBE'
  },
  silent: true
});

// 6.为圆cicle绑定形状动画,参数true表示循环执行
cr.animate('shape', true)
  .when(5000, {
    cx: w - r
  })
  .when(10000, {
    cx: r
  })
  .start();

// 7.将圆形对象实例cricle添加到zr实例中进行渲染
zr.add(cr);

ZRender绘制流程

以下是上面案例 ZRender 的绘制流程图:

在这里插入图片描述

1)创建 ZRender 实例

使用const zr = zrender.init(),可多 zr 实例,每个实例都拥有自己的画布

2)创建需要绘制的图形实例

图形类名可通过zrender.xxx获得,其中 xxx 为图形类名

3)zr.add 方法将图形实例添加到存储器

// zrender.ts
add(el: Element) {
  // 将el(这里为cr实例)添加到存储器
  this.storage.addRoot(el);

  // 并且将动画放入动画管理器
  el.addSelfToZr(this);

  // 启动绘制程序
  this.refresh();
}

3B、C、D:将图形上绑定的动画添加到动画管理器,生成动画帧,启动动画绘制

4)zr 实例化时就已经启动逐帧扫描程序

这里存储器有可渲染的元素被捕获后才会执行渲染动作

// zrender.ts
class Zrender {
  constructor() {
    this.animation = new Animation({
      stage: {
        // 将渲染程序绑定到帧渲染策略
        update: () => this._flush(true)
      }
    });
    // 启动动画管理器,启动帧渲染扫描rAF程序
    this.animation.start();
  }

  // 下一帧执行渲染
  _flush() {
    this.refreshHoverImmediately();
  }

  // 强制渲染
  refreshHoverImmediately() {

    // 调用渲染器渲染程序
    this.painter.refresh();
  }
}

5)上一步的 this.panter.refresh() 会请求 storage 去获取渲染列表

// canvas/Painter.ts
class Painter {
  refresh() {
    // 获取渲染列表
    const list = this.storage.getDisplayList(true);
  }
}

// Storage.ts
class Storage {
  /**
    * 更新图形的绘制队列。
    * 每次绘制前都会调用,该方法会先深度优先遍历整个树,
    * 更新所有Group和Shape的变换并且把所有可见的Shape保存到数组中,
    * 最后根据绘制的优先级(zlevel > z > 插入顺序)排序得到绘制队列
    */
  getDisplayList() {
    // 返回渲染列表
    return this._displayList
  }

6)启动并执行渲染程序,进行图形的路径绘制

主要方法为:doPaintList -> doPaintEl

Logo

为武汉地区的开发者提供学习、交流和合作的平台。社区聚集了众多技术爱好者和专业人士,涵盖了多个领域,包括人工智能、大数据、云计算、区块链等。社区定期举办技术分享、培训和活动,为开发者提供更多的学习和交流机会。

更多推荐