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 'svg
devicePixelRatio?: number
width?: number | string // 10, 10px, 'auto'
height?: number | string
useDirtyRect?: boolean
useCoarsePointer?: 'auto' | boolean
pointerSize?: number
ssr?: boolean // If enable ssr mode.
}
class ZRender {
dom?: HTMLElement // 如果是ssr-svg渲染,可以不用传dom
id: number
storage: 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.`);
}
}
// 这里我们遇到了文档没说明的useDirtyRect
opts.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,painter
this.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 版本起支持:是否扩大可点击元素的响应范围。
// `'auto'` 表示对移动设备开启;`true` 表示总是开启;`false` 表示总是不开启。
const useCoarsePointer = opts.useCoarsePointer;
// 扩大元素响应范围的像素大小,配合 `opts.useCoarsePointer` 使用。
const usePointerSize = (useCoarsePointer == null || useCoarsePointer === 'auto')
? env.touchEventsSupported
: !!useCoarsePointer;
const defaultPointerSize = 44;
let pointerSize;
if (usePointerSize) {
pointerSize = zrUtil.retrieve2(opts.pointerSize, defaultPointerSize);
}
// 初始化handler
this.handler = new Handler(storage, painter, handerProxy, painter.root, pointerSize);
// 初始化animation
this.animation = new Animation({
stage: {
update: ssrMode ? null : () => this._flush(true)
}
});
if (!ssrMode) {
this.animation.start();
}
}
}
🤔 这里看源码时遇到几个疑问:
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 flushing,中间绿光在闪说明画布在更新:
未开启前,当更新的图形不在区域但还是会更新
开启后,更新图形不在区域内不会更新
于是我找了这个功能的 commit,在后面的分析中会写到这个 useDirtyRect 的工作流程。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情