🦊 源码解读之zrender-Zrender类(3)

556 阅读3分钟

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 上了。

image.png

  • 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,中间绿光在闪说明画布在更新:

未开启前,当更新的图形不在区域但还是会更新

iShot_2023-02-06_15.28.36.gif

开启后,更新图形不在区域内不会更新

iShot_2023-02-06_15.36.40.gif

于是我找了这个功能的 commit,在后面的分析中会写到这个 useDirtyRect 的工作流程。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情