人间熙攘,好久不见

vuePress-theme-reco Magic    2017 - 2023
人间熙攘,好久不见 人间熙攘,好久不见

Choose mode

  • dark
  • auto
  • light
Home
Category
  • tools
  • Zsh
  • iTerm2
  • Front-end
  • 版本控制-Git
  • Rust
  • skia-safe
  • 第三方对接
  • MQTT
  • Powershell
  • python
  • MD5
  • SHA1
  • wsl2
Tags
TimeLine
GitHub
  • Github (opens new window)
  • Before (opens new window)
author-avatar

Magic

23

文章

15

标签

Home
Category
  • tools
  • Zsh
  • iTerm2
  • Front-end
  • 版本控制-Git
  • Rust
  • skia-safe
  • 第三方对接
  • MQTT
  • Powershell
  • python
  • MD5
  • SHA1
  • wsl2
Tags
TimeLine
GitHub
  • Github (opens new window)
  • Before (opens new window)
  • FrontEnd

    • 使用 Vue + TypeScript 开发 Chrome Extensions 🐱‍👤
    • Debounce Throttling ~
    • 从 Echarts 看 Zrender 的使用
    • Performance Yes 🎉
    • Rollup + Typescript => NPM Package
    • Svelte 事件问题

从 Echarts 看 Zrender 的使用

vuePress-theme-reco Magic    2017 - 2023

从 Echarts 看 Zrender 的使用

Magic 2022-05-15 Front-endEchartsZrender

performance

# 从一个 俄罗斯方块看 Echarts 对 Zrender 的使用方式

来源还是以最近入职了一家新公司,让我使用 Zrender 来写一个俄罗斯方块,因为这方面比较陌生,所以给予 google 搜索了一下,基本都是以 Echarts 来进行的,所以不得意 查看 Zrender 的文档手撸了一个,在过程中发现 Zrender 的文档是写的真烂,有些还不是最新的,需要基于 源码 来进行推断 💩,最近正好有时间,所以看下 Echarts 是怎么对 Zrender 再次封装使用的。。。

tetris
  • 话不多说,开始






 




 



<template>
  <div class="tetris" ref="tetris"></div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as Echarts from 'echarts';

const tetris = ref(null);

onMounted(() => {
  const myChat = Echarts.init(tetris.value!);
  var refreshT: any, fallBlockT: any;
});

可以看到 Echarts.init (opens new window) 来进行初始化的,并返回这个实例











 












export function init(
    dom: HTMLElement,
    theme?: string | object,
    opts?: EChartsInitOpts
): EChartsType {
    const isClient = !(opts && opts.ssr);
    if (isClient) {
        ...
        // 一堆判断逻辑
    }
    const chart = new ECharts(dom, theme, opts); // https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L331
    chart.id = 'ec_' + idBase++; // 是一个 时间戳
    instances[chart.id] = chart; // instances 是一个 {[id: string]: ECharts}
    // 这里给 dom 设置属性 -> _echarts_instance_="ec_1652617475180
    isClient && modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id);

    enableConnect(chart); // https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L307:5

    lifecycle.trigger('afterinit', chart); // https://github.com/apache/echarts/blob/master/src/core/lifecycle.ts#L66
    // 注册 afterinit 函数位置:https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L2785

    return chart;

可以看到内部实例化了一个 ECharts (opens new window),我们看 ECharts 类中构造函数 (opens new window)都干了什么?













 
 
 
 
 
 
 
 
































 
 
 
 
 
 






class ECharts extends Eventful<ECEventDefinition> {
  ...
  constructor(
        dom: HTMLElement,
        // Theme name or themeOption.
        theme?: string | ThemeOption,
        opts?: EChartsInitOpts
    ) {
        // 实例化了一个关于 事件处理函数,通过查看文件发现是基于 zrender/src/core/Eventful 来的,可以看到其注释是用来 查询 事件来的
        super(new ECEventProcessor());
        ...
        // 基于 zrender 实例化了一个 zr
        const zr = this._zr = zrender.init(dom, {
            renderer: opts.renderer || defaultRenderer,
            devicePixelRatio: opts.devicePixelRatio,
            width: opts.width,
            height: opts.height,
            ssr: opts.ssr,
            useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
        });
        ...
        theme && backwardCompat(theme as ECUnitOption, true); // 处理主题兼容性 https://github.com/apache/echarts/blob/master/src/preprocessor/backwardCompat.ts#L144

        this._theme = theme;

        this._locale = createLocaleObject(opts.locale || SYSTEM_LANG); // 合并默认配置 https://github.com/apache/echarts/blob/master/src/core/locale.ts#L55

        // 基于 zrender/src/core/util 进行一系列事件操作 https://github.com/apache/echarts/blob/master/src/core/CoordinateSystem.ts#L28
        this._coordSysMgr = new CoordinateSystemManager();

        // https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L2474
        const api = this._api = createExtensionAPI(this);

        // Sort on demand
        function prioritySortFunc(a: StageHandlerInternal, b: StageHandlerInternal): number {
            return a.__prio - b.__prio;
        }
        // 使用 zrender https://github.com/ecomfe/zrender/blob/e25b11095c9861d0293a8a7d13199581942544f9/src/core/timsort.ts#L634
        timsort(visualFuncs, prioritySortFunc);
        timsort(dataProcessorFuncs, prioritySortFunc);

        // 这也是一个操作调度器,封装了一些操作 https://github.com/apache/echarts/blob/master/src/core/Scheduler.ts#L102
        this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);

        this._messageCenter = new MessageCenter();

        // Init mouse events https://github.com/apache/echarts/blob/master/src/core/echarts.ts#L1045 可以看到是其内部私有方法,一些是基于 zrender.on ,一些是基于 内部实现的 trigger 来触发,最后使用 handleLegacySelectEvents 来处理消息中心一的事件
        this._initEvents();

        // In case some people write `window.onresize = chart.resize`
        this.resize = bind(this.resize, this);

        // 在这里调用了 zrender 的 动画监听器 https://github.com/ecomfe/zrender/blob/67c46f39c208bb86df84c0afa63399755747f4ff/src/animation/Animation.ts#L1
        zr.animation.on('frame', this._onframe, this);

        // 下面两个通过 zr.on 来监听渲染事件和移动事件和点击事件 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L2009
        bindRenderedEvent(zr, this);
        bindMouseEvent(zr, this); // 这里内部 调用了 findComponentHighDownDispatchers 方法 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/states.ts#L607:17

        // ECharts instance can be used as value.
        setAsPrimitive(this); // 这里调用了 zrender/src/core/util 的方法 https://github.com/ecomfe/zrender/blob/e25b11095c9861d0293a8a7d13199581942544f9/src/core/util.ts#L648
    }
}

接着看 俄罗斯方块 实现代码,可以看到主要都是通过 myChat.setOption 这个方法来更新的

refreshT = function() {
  var pts = (firstBlock.norBase as any).clone();
  if (fallLine < 0 || touchFallOther()) {
    ...
    myChat.setOption(option);
    ...
  } else {
    ...
  }
  myChat.setOption(option);
};

那来看下 myChat.setOption 都在干什么,具体代码在 setOption (opens new window),可以看到 有函数重载,针对不同的入参有不同的逻辑:

/**
     * Usage:
     * chart.setOption(option, notMerge, lazyUpdate);
     * chart.setOption(option, {
     *     notMerge: ...,
     *     lazyUpdate: ...,
     *     silent: ...
     * });
     *
     * @param opts opts or notMerge.
     * @param opts.notMerge Default `false`.
     * @param opts.lazyUpdate Default `false`. Useful when setOption frequently.
     * @param opts.silent Default `false`.
     * @param opts.replaceMerge Default undefined.
     */
    // Expose to user full option.
    // ECBasicOption -> https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/types.ts#L575
    // ECUnitOption -> https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/util/types.ts#L519:13 可以看到关键的 interface 定义
    setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean, lazyUpdate?: boolean): void;
    setOption<Opt extends ECBasicOption>(option: Opt, opts?: SetOptionOpts): void;
    /* eslint-disable-next-line */
    setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
        ...

        // 这里判断 this._model 不存在会重新初始化,因为后续步骤会用到这个,_model 是echart的私有变量,可以在 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/core/echarts.ts#L350 看到,所以这个 _model 是在第一次 setOption 的时候设置的
        if (!this._model || notMerge) {
            // https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L58
            const optionManager = new OptionManager(this._api);
            const theme = this._theme;
            // https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L154
            const ecModel = this._model = new GlobalModel();
            ecModel.scheduler = this._scheduler;
            ecModel.ssr = this._ssr;
            // https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L199
            ecModel.init(null, null, null, theme, this._locale, optionManager);
        }

        this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);

        const updateParams = {
            seriesTransition: transitionOpt,
            optionChanged: true
        } as UpdateLifecycleParams;

        if (lazyUpdate) {
            ...
        }
        else {
            try {
                prepare(this);
                updateMethods.update.call(this, null, updateParams);
            }
            catch (e) {
                this[PENDING_UPDATE] = null;
                this[IN_MAIN_PROCESS_KEY] = false;

                throw e;
            }

            // Ensure zr refresh sychronously, and then pixel in canvas can be
            // fetched after `setOption`.
            if (!this._ssr) {
                // not use flush when using ssr mode.
                this._zr.flush();
            }

            this[PENDING_UPDATE] = null;
            this[IN_MAIN_PROCESS_KEY] = false;

            flushPendingActions.call(this, silent);
            triggerUpdatedEvent.call(this, silent);
        }
    }

关键代码 :

// 1. 实际调用 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/Global.ts#L214
// 1.2 接下来调用 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L93:5 实际是 `optionManager` 下的 setOption 来重绘,
// 1.3. 在 `optionManager -> setOption` 下调用 zrdenr 的 each 方法进行遍历,并调用了 `zrender -> setAsPrimitive` 方法, 应该是是通过设置 `__ec_primitive__  = true` 来进行性能优化吧
// 1.4 使用 clone 对 optionObj 深拷贝来返回一个新的对象进行修改
// 1.5 使用 parseRawOption 对配置选项预处理 https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/model/OptionManager.ts#L300:10
// 1.6 内部针对 连续 多次 setoption 时 对 时间轴(timelineOptions) 和 移动端自适应(media) 特殊处理,不进行合并,而是直接替换为新值
// 1.7 最后 newParsedOption 配置 赋值到 _optionBackup 上
// 2. 最后 内部调用 `resetOption` 重置所有配置,跟传入参数 `recreate` 一致
this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);

再看后续 else 的逻辑:

else {
    try {
        prepare(this);
        updateMethods.update.call(this, null, updateParams);
    }
    catch (e) {
        this[PENDING_UPDATE] = null;
        this[IN_MAIN_PROCESS_KEY] = false;

        throw e;
    }

    // Ensure zr refresh sychronously, and then pixel in canvas can be
    // fetched after `setOption`.
    if (!this._ssr) {
        // not use flush when using ssr mode.
        this._zr.flush();
    }

    this[PENDING_UPDATE] = null;
    this[IN_MAIN_PROCESS_KEY] = false;

    flushPendingActions.call(this, silent);
    triggerUpdatedEvent.call(this, silent);
}
  • 可以看到调用了 prepare (opens new window) 方法,内部其实调用了 _scheduler (opens new window) 和 prepareView (opens new window),其中 _scheduler 就是在 echarts 初始化方法中赋值的 this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);;

    其中 prepareView 中使用 eachSeries (opens new window) 来处理 series 相关数据,其中 _componentsMap (opens new window) 在创建的时候就已经默认声明了 series 属性了

    关键代码 doPrepare (opens new window):

    function doPrepare(model: ComponentModel): void {
        // By defaut view will be reused if possible for the case that `setOption` with "notMerge"
        // mode and need to enable transition animation. (Usually, when they have the same id, or
        // especially no id but have the same type & name & index. See the `model.id` generation
        // rule in `makeIdAndName` and `viewId` generation rule here).
        // But in `replaceMerge` mode, this feature should be able to disabled when it is clear that
        // the new model has nothing to do with the old model.
        const requireNewView = model.__requireNewView;
        // This command should not work twice.
        model.__requireNewView = false;
        // Consider: id same and type changed.
        const viewId = '_ec_' + model.id + '_' + model.type;
        let view = !requireNewView && viewMap[viewId];
        if (!view) {
            const classType = parseClassType(model.type);
            // 如果 series 的 type 为 scatter, 那后续 new Clazz() 就是 new Scatter
            const Clazz = isComponent
                ? (ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub)
                : (
                    // FIXME:TS
                    // (ChartView as ChartViewConstructor).getClass('series', classType.sub)
                    // For backward compat, still support a chart type declared as only subType
                    // like "liquidfill", but recommend "series.liquidfill"
                    // But need a base class to make a type series.
                    (ChartView as ChartViewConstructor).getClass(classType.sub)
                );
    
            if (__DEV__) {
                assert(Clazz, classType.sub + ' does not exist.');
            }
    
            view = new Clazz();
            // 把 一些 扩展 api 挂在到当前实例上
            view.init(ecModel, api);
            // 缓存下
            viewMap[viewId] = view;
            viewList.push(view as any);
            // 添加到 画布上
            zr.add(view.group);
        }
    
        model.__viewId = view.__id = viewId;
        view.__alive = true;
        view.__model = model;
        view.group.__ecComponentInfo = {
            mainType: model.mainType,
            index: model.componentIndex
        };
        !isComponent && scheduler.prepareView(
            view as ChartView, model as SeriesModel, ecModel, api
        );
    }
    

    关键代码:

    for (let i = 0; i < viewList.length;) {
        const view = viewList[i];
        if (!view.__alive) {
            // 不是 活动的就销毁掉 并从 数组中移除
            !isComponent && (view as ChartView).renderTask.dispose();
            zr.remove(view.group);
            view.dispose(ecModel, api);
            viewList.splice(i, 1);
            if (viewMap[view.__id] === view) {
                delete viewMap[view.__id];
            }
            view.__id = view.group.__ecComponentInfo = null;
        }
        else {
            i++;
        }
    }
    
  • 接下来可以看到 updateMethods.update.call(this, null, updateParams); (opens new window) 是一个 更新方法

    其中在 update 有一个方法 render(this, ecModel, api, payload, updateParams); (opens new window),可以根据名称猜到这个应该是 渲染 相关的,其中 render (opens new window) 赋值在这里

    render = (
        ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
        updateParams: UpdateLifecycleParams
    ) => {
        allocateZlevels(ecModel);
    
        renderComponents(ecIns, ecModel, api, payload, updateParams);
    
        each(ecIns._chartsViews, function (chart: ChartView) {
            chart.__alive = false;
        });
    
        renderSeries(ecIns, ecModel, api, payload, updateParams);
    
        // Remove groups of unrendered charts
        each(ecIns._chartsViews, function (chart: ChartView) {
            if (!chart.__alive) {
                chart.remove(ecModel, api);
            }
        });
    };
    

    有一些内部方法是基于 接口实现编程,比如 class CandlestickView extends ChartView { (opens new window),这样就可以在 updateZ (opens new window) 直接使用了

    function updateZ(model: ComponentModel, view: ComponentView | ChartView): void {
        if (model.preventAutoZ) {
            return;
        }
        const z = model.get('z') || 0;
        const zlevel = model.get('zlevel') || 0;
        // Set z and zlevel
        view.eachRendered((el) => {
            doUpdateZ(el, z, zlevel, -Infinity);
            // Don't traverse the children because it has been traversed in _updateZ.
            return true;
        });
    };
    

    renderSeries (opens new window) 可以看待注释是 渲染 图表 和 组件

    // 是调用 对应组建模块的 render 的方法 例如:https://github.com/apache/echarts/blob/84b957855ba6fc7f5d81fdd11b58d85f16089998/src/component/calendar/CalendarView.ts#L61
    componentView.render(componentModel, ecModel, api, payload);
    

    可以看到 有些内部常见 React (opens new window) 是通过 graphic.Rect 构建的:















     
     
     
     
     
     
     
     
     
     
     






    // render day rect
    _renderDayRect(calendarModel: CalendarModel, rangeData: CalendarParsedDateRangeInfo, group: graphic.Group) {
        const coordSys = calendarModel.coordinateSystem;
        const itemRectStyleModel = calendarModel.getModel('itemStyle').getItemStyle();
        const sw = coordSys.getCellWidth();
        const sh = coordSys.getCellHeight();
    
        for (let i = rangeData.start.time;
            i <= rangeData.end.time;
            i = coordSys.getNextNDay(i, 1).time
        ) {
    
            const point = coordSys.dataToRect([i], false).tl;
    
            // every rect
            const rect = new graphic.Rect({
                shape: {
                    x: point[0],
                    y: point[1],
                    width: sw,
                    height: sh
                },
                cursor: 'default',
                style: itemRectStyleModel
            });
    
            group.add(rect);
        }
    
    }
    

    那么 graphic 这个里面是什么呢?,下面来看看一下 util/graphic.ts (opens new window)

    ...
    import Circle from 'zrender/src/graphic/shape/Circle';
    import Ellipse from 'zrender/src/graphic/shape/Ellipse';
    import Sector from 'zrender/src/graphic/shape/Sector';
    import Ring from 'zrender/src/graphic/shape/Ring';
    import Polygon from 'zrender/src/graphic/shape/Polygon';
    import Polyline from 'zrender/src/graphic/shape/Polyline';
    import Rect from 'zrender/src/graphic/shape/Rect';
    ...
    

    可以看到是使用 zrender 里面方法构建对应数据的,至此关键代码基本完成,再次推荐一个插件方便查看源码:Sourcegraph (opens new window)

欢迎来到 人间熙攘,好久不见
看板娘