背景
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