Echarts-ZRender源码分析
Posted 知前端
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Echarts-ZRender源码分析相关的知识,希望对你有一定的参考价值。
导读:ZRender是Echarts的底层图形绘制引擎,它是一个独立发布的基于Canvas/SVG/VML的2D图形绘制引擎,提供功能:
图形绘制及管理(CRUD、打组) 图形(包含文字)动画管理 图形(包含文字)事件管理(canvas中实现dom事件) 基于“响应式”(dirty标记)的高效帧渲染机制 可选择的渲染器机制(Canvas/SVG/VML(5.0已放弃VML支持)) Tips:图形特指2D矢量图型
1.整体架构
1.1 基于MVC模式整体架构
如上图所示,ZRender是整体设计思路是面向对象的MVC模式,视图层负责渲染,控制层负责用户输入交互,数据层负责数据模型的编排与存储,其对应的文件和作用如下:
-
Storage.ts(数据模型):用于存储所有需要绘制的图形数据,并且提供相关数据的LRU缓存机制,提供数据的CURD管理; -
PainterBase.ts(视图绘制):PainterBase是绘制的基类,系统提供的Canvas、SVG、VML视图绘制类都继承于PainterBase类,用户也可以自行继承实现如webgl的绘制能力; -
Handler.ts(交互控制):事件交互控制模块,为图形元素实现和htmlDOMElement一样的事件交互逻辑,如图形的选中、单击、触摸等事件;
除了上述MVC3大模块以外,还有以下辅助功能模块:
1.2 辅助功能模块
-
动画管理模块(animation):管理图形的动画,绘制前会将对象的动画计算成帧对象保存在动画管理器中,伴随着动画触发条件将帧数据推送给视图绘制模块进行动画绘制; -
工具类模块(tool、core):提供颜色转换、路径转换、变换矩阵运算、基础事件对象封装、向量计算、基础数据结构等独立辅助计算函数或者类; -
图形对象模块(graphic):提供元素的对象类(包含Image、Group、Arc、Rect等),所有元素其最顶层都继承于Element类; -
图形对象辅助模块(contain):提供用于判断包含关系的算法,比如:坐标点是否在线上,坐标点是否在图形内;
Tips:上文中“元素”包含Group和2D图形,而图形只包含2D图形,不包含Group
2.源码文件结构
2.1 源码目录结构
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
2.2 目录及文件介绍
-
config.ts:全局配置文件,可配置debug模式、retina屏幕高清优化、深/浅主题色值等 -
Element.ts:所有可绘制图形元素和Group的基类,其中定义了基础属性(如:id,name,type,isGroup等),对象的基础成员方法(hidden,show,animate,animateTo,copyValue等) -
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入口文件,也是编译主入口, -
暴露全局方法:init用于初始化ZRender实例,delInstance用于删除ZRender实例,dispose用于注销某个ZRender实例,disposeAll用于注销所有ZRender实例,registerPainter用于注册新的渲染器 -
ZRender类:用于管理ZRender实例里的所有元素对象实例,存储器(Storage)实例,渲染器(Painter)实例,事件控制器(Handler)实例,动画管理器(Animation)实例 -
export.ts:编译时调用,用于对外导出API -
animation:存放动画相关的代码文件,如:Animation,Animator等 -
canvas:存放Canvas渲染器相关的代码文件 -
svg:存放svg渲染器相关的程序文件 -
vml:存放vml渲染器相关的程序文件 -
contain:用于补充特殊元素的坐标包含关系计算方法,如贝塞尔曲线上的点包含关系计算 -
core:大杂烩文件夹,我这里把它归纳为工具方法文件,包含LRU缓存,包围盒计算,浏览器环境判断,变换矩阵,触摸事件实现等大杂烩方法 -
dom:仅HandlerProxy.ts一个程序文件,用于实现DOM事件代理,所有画布内元素的事件都是从画布DOM的事件进行代理进入 -
graphic:所有元素的实体对象类都存放在这个文件夹,包含Group,可绘制对象基类Displayable,路径,圆弧,矩形等 -
mixin:仅Draggable.ts一个文件,用于管理元素的拖拽事件,因为Echarts用不上拖拽,所以拖拽事件还没有在ts版本中实现(后面会分享个人实现的版本代码) -
tool:工具方法,提供颜色计算,SVG路径转换等工具类
3.入口文件源码分析(zrender.ts)
3.1 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 {
// 后文详解
}
3.2 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);
// 将存储器实例赋值给成员变量(作者认为这步是脱了裤子放屁,还多const一个storage变量)
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() { }
}
4.通过案例分析ZRender工作流程
4.1 案例
下面代码是绘制一个半径为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);
4.2 ZRender绘制流程
这一节主要结合2.1的案例讲ZRender如何进行绘制和运行动画流程
-
创建ZRender实例:使用const zr = zrender.init(),可多zr实例,每实例拥有自己的画布 -
创建需要绘制的图形实例,图形类名可通过zrender.xxx获得,其中xxx为图形类名 -
zr.add方法将图形实例添加到存储器
// zrender.ts
add(el: Element) {
// 将el(这里为cr实例)添加到存储器
this.storage.addRoot(el);
// 并且将动画放入动画管理器
el.addSelfToZr(this);
// 启动绘制程序
this.refresh();
}
3B、C、D. 将图形上绑定的动画添加到动画管理器,生成动画帧,启动动画绘制
-
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();
}
}
-
上一步的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、7启动并执行渲染程序,进行图形的路径绘制,主要方法为:doPaintList->doPaintEl
本章节END.
ZRender源码分析后续章节待续:
元素对象源码解析 事件管理器源码解析 动画管理器源码解析 渲染器源码解析
雷庭,任职于北京优锘科技,前端架构师,从事前端开发及架构工作17年,擅长可视化领域的前端开发,前端沟通交流可加作者微信ltlt820706
以上是关于Echarts-ZRender源码分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段
mysql jdbc源码分析片段 和 Tomcat's JDBC Pool
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段