00 小结
当我们在 zrender.init(document.getElementById(“canvas”))时,首先实例化了一个 ZRender 实例,在这个实例化过程中,主要实例化了:
- Storage 类,作用类似于全局状态管理
- Painter 类,可以理解为画笔(渲染模式),目前支持svg和canvas模式,默认是canvas
- Handler 类,先简单理解为用户处理器,它把数据storage和画笔painter的实例都传进去了
- Animation 类,简单理解为处理图形的位置,形状
在实例化过程中,还遇到了几个文档描述比较模糊但很有用的参数:useDirtyRect,useCoarsePointer,usePointerSize。
01 代码分析
代码太长,部分省略,只分析主流程
// src/zrender.ts
export interface ZRenderInitOpt {renderer?: string // 'canvas' or 'svgdevicePixelRatio?: numberwidth?: number | string // 10, 10px, 'auto'height?: number | stringuseDirtyRect?: booleanuseCoarsePointer?: 'auto' | booleanpointerSize?: numberssr?: boolean // If enable ssr mode.
}class ZRender {dom?: HTMLElement // 如果是ssr-svg渲染,可以不用传domid: numberstorage: Storage // 数据中心painter: PainterBase // 视图handler: Handler // 事件处理animation: Animation // 动画constructor(id: number, dom?: HTMLElement, opts?: ZRenderInitOpt) {// 初始化储存中心和画布相关属性opts = opts || {};this.dom = dom;this.id = id;const storage = new Storage();let rendererType = opts.renderer || 'canvas';if (!painterCtors[rendererType]) {// 使用第一个注册的渲染器rendererType = zrUtil.keys(painterCtors)[0];}// 如果不是生成环境 抛出错误,👆上面代码做生产兜底if (process.env.NODE_ENV !== 'production') {if (!painterCtors[rendererType]) {throw new Error(`Renderer '${rendererType}' is not imported. Please import it first.`);}}// 这里我们遇到了文档没说明的useDirtyRectopts.useDirtyRect = opts.useDirtyRect == null? false: opts.useDirtyRect;// 画笔实例(svg/canvas)const painter = new painterCtors[rendererType](dom, storage, opts, id);// ssr模式,没有使用过,暂忽略。const ssrMode = opts.ssr || painter.ssrOnly;// 初始化zrender中的storage,painterthis.storage = storage;this.painter = painter;// 如果不是env.node, env.worker, ssrMode,初始化handerProxy。const handerProxy = (!env.node && !env.worker && !ssrMode)? new HandlerProxy(painter.getViewportRoot(), painter.root): null;// useCoarsePointer,5.4.0 版本起支持:是否扩大可点击元素的响应范围。const useCoarsePointer = opts.useCoarsePointer;// 扩大元素响应范围的像素大小,配合 `opts.useCoarsePointer` 使用。const usePointerSize = (useCoarsePointer == null || useCoarsePointer === 'auto')? env.touchEventsSupported: !!useCoarsePointer;const defaultPointerSize = 44;let pointerSize;if (usePointerSize) {// 默认是44,如果传了pointerSize,那就是pointerSizepointerSize = zrUtil.retrieve2(opts.pointerSize, defaultPointerSize);}// 初始化handlerthis.handler = new Handler(storage, painter, handerProxy, painter.root, pointerSize);// 初始化animationthis.animation = new Animation({stage: {update: ssrMode ? null : () => this._flush(true)}});if (!ssrMode) {this.animation.start();}}
}
🤔 03 问题
看源码时遇到几个疑问:
if(!painterCtors[rendererType])
这里的 painterCtors 初始值为const painterCtors: Dictionary<PainterBaseCtor> = {}
,咋一看也没看到它在哪里赋初值,然后在网页打断点调试,发现在初始化的时候会调用 registerPainter
export function registerPainter(name: string, Ctor: PainterBaseCtor) {painterCtors[name] = Ctor;
}// all.ts
import { registerPainter } from './zrender';
import CanvasPainter from './canvas/Painter';
import SVGPainter from './svg/Painter';
registerPainter('canvas', CanvasPainter);
registerPainter('svg', SVGPainter);
这时候已经把 canvas,svg 的 painter 注册在全局变量 painterCtors 上了。
opts.useDirtyRect
,这个属性在zrender文档上没找到具体的说明,通过源码可知 opts 传递给了 Painter 类:
// zrender.ts
opts.useDirtyRect = opts.useDirtyRect == null? false: opts.useDirtyRect;const painter = new painterCtors[rendererType](dom, storage, opts, id);
在 CavansPainter 这个类里,useDirtyRect 这个属性主要在 refresh, _doPaintList, _doPaintEl
这几个函数中被使用了,这几个方法主要是控制画布是否更新,根据函数名可以猜测这个属性的功能是"脏矩形渲染",也就是局部渲染,找到图形会变化的区域(脏矩形)做去更新。这个区域外都是不变的。
refresh(paintAll?: boolean) {const list = this.storage.getDisplayList(true);// 省略部分代码...if (this._opts.useDirtyRect) {this._prevDisplayList = list.slice();}return this;
}private _doPaintList(...){// ...const repaintRects = useDirtyRect&& layer.createRepaintRects(list, prevList, this._width, this._height);// ...this._doPaintEl(...);// ...return { finished, needsRefreshHover };
}private _doPaintEl(...) {const ctx = currentLayer.ctx;if (useDirtyRect) {const paintRect = el.getPaintRect();if (!repaintRect || paintRect && paintRect.intersect(repaintRect)) {brush(ctx, el, scope, isLast);el.setPrevPaintRect(paintRect);}}else {brush(ctx, el, scope, isLast);}
}
我做了一个实验来验证这个属性,开启 rendering 中的 paint flashing,中间绿光在闪说明画布在更新:
未开启前,当更新的图形不在区域但还是会更新
开启后,更新图形不在区域内不会更新
于是我找了这个功能的 commit,在后面的分析中会写到这个 useDirtyRect 的工作流程。
- useCoarsePointer 和 usePointerSize,主要是 usePointerSize,有些线条比较细,可以使用这个属性加大响应范围。zrender test 实例
// useCoarsePointer,5.4.0 版本起支持:是否扩大可点击元素的响应范围。
const useCoarsePointer = opts.useCoarsePointer;
// 扩大元素响应范围的像素大小,配合 `opts.useCoarsePointer` 使用。
const usePointerSize = (useCoarsePointer == null || useCoarsePointer === 'auto')? env.touchEventsSupported: !!useCoarsePointer;const defaultPointerSize = 44;
let pointerSize;
if (usePointerSize) {// 默认是44,如果传了pointerSize,那就是pointerSizepointerSize = zrUtil.retrieve2(opts.pointerSize, defaultPointerSize);
}
// 初始化handler
this.handler = new Handler(storage, painter, handerProxy, painter.root, pointerSize);// 在Handler.ts里,这个值被用来计算包围盒的大小
const pointerRect = new BoundingRect(x - targetSizeHalf, y - targetSizeHalf, pointerSize, pointerSize);
- animation.start() 开启动画,利用 requestAnimationFrame。
start() {if (this._running) return;this._time = getTime();this._pausedTime = 0;this._startLoop();
}_startLoop() {const self = this;this._running = true;function step() {if (self._running) {// 递归调用requestAnimationFrame(step);// 没有暂停更新的情况下就执行update!self._paused && self.update();}}// 下次重绘之前调用 step 回调函数更新动画requestAnimationFrame(step);
}