Echarts -- ZRender 源码分析

背景

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.tsM层,对象模型层/存储器层,存储并管理元素对象实例,元素对象实例存储在 _displayableList 数组中,每次绘制时会根据 zlevel -> z -> 插入顺序进行排序,提供添加、删除、清空注销元素对象实例的方法
Handler.tsC层,控制层/器,用于向元素上绑定事件,实现 DOM 式事件管理机制
PainterBase.tsV层,视图层/渲染器层,PainterBase 是渲染器的基类,5.0 版本默认提供 Canvas、SVG 渲染器,5.0 之前版本还提供 VML 渲染器,元素的绘制就是由渲染器决定,系统默认 Canvas 渲染器渲染
zrender.tsZRender 入口文件,也是编译主入口
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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值