echarts5图表渲染过程分析

843 阅读7分钟

Echarts快速入门

Echarts就不做过多介绍了,相信大家都听说或者使用过,现在以官方给的第一个快速入门为基础,分析一下渲染过程。示例代码如下

<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">

    
    var myChart = echarts.init(document.getElementById('main'));

    
    var option = {
      title: {
        text: 'ECharts 入门示例'
      },
      tooltip: {},
      legend: {
        data: ['销量']
      },
      xAxis: {
        data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
      },
      yAxis: {},
      series: [
        {
          name: '销量',
          type: 'bar',
          data: [5, 20, 36, 10, 10, 20]
        }
      ]
    };

    
    myChart.setOption(option);
  </script>

渲染出来的效果就是这样的

代码分析

echarts是依赖zrender来绘制的,上述代码可以看到调用了echarts的2个方法,initsetOption,5.x版本的echarts是使用typescript写的。
先看下init方法的定义,在src/echarts.ts里,为了更简洁清晰,后续代码我删掉了非主干流程渲染的部分内容。

import { init } from './core/echarts';
export default {
    init() {
        return init.apply(null, arguments);
    }
};

真正的init方法其实是./core/echarts里的

export function init(
    dom: HTMLElement,
    theme?: string | object,
    opts?: EChartsInitOpts
): EChartsType {
    const existInstance = getInstanceByDom(dom);
    if (existInstance) {
         return existInstance;
    }
    const chart = new ECharts(dom, theme, opts);
    chart.id = 'ec_' + idBase++;
    instances[chart.id] = chart;

    modelUtil.setAttribute(dom, DOM_ATTRIBUTE_KEY, chart.id);

    enableConnect(chart);

    lifecycle.trigger('afterinit', chart);

    return chart;
}

代码实例化了ECharts对象,构造函数代码

class ECharts extends Eventful<ECEventDefinition> {
	 constructor(
	        dom: HTMLElement,
	        
	        theme?: string | ThemeOption,
	        opts?: EChartsInitOpts
	    ) {
	        super(new ECEventProcessor());
	
	        opts = opts || {};
	
	        
	        if (typeof theme === 'string') {
	            theme = themeStorage[theme] as object;
	        }
	
	        this._dom = dom;
	
	        let defaultRenderer = 'canvas';
	        let defaultUseDirtyRect = false;
	        
	        const zr = this._zr = zrender.init(dom, {
	            renderer: opts.renderer || defaultRenderer,
	            devicePixelRatio: opts.devicePixelRatio,
	            width: opts.width,
	            height: opts.height,
	            useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
	        });
	
	        
	        this._throttledZrFlush = throttle(bind(zr.flush, zr), 17);
	
	        theme = clone(theme);
	        theme && backwardCompat(theme as ECUnitOption, true);
	
	        this._theme = theme;
	
	        this._locale = createLocaleObject(opts.locale || SYSTEM_LANG);
	
	        this._coordSysMgr = new CoordinateSystemManager();
	
	        const api = this._api = createExtensionAPI(this);
	
	        
	        function prioritySortFunc(a: StageHandlerInternal, b: StageHandlerInternal): number {
	            return a.__prio - b.__prio;
	        }
	        timsort(visualFuncs, prioritySortFunc);
	        timsort(dataProcessorFuncs, prioritySortFunc);
	
	        this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);
	
	        this._messageCenter = new MessageCenter();
	
	        
	        this._initEvents();
	
	        
	        this.resize = bind(this.resize, this);
	
	        zr.animation.on('frame', this._onframe, this);
	
	        bindRenderedEvent(zr, this);
	
	        bindMouseEvent(zr, this);
	
	        
	        setAsPrimitive(this);
	  }
    }

内容比较多,重点代码是初始化了zrender

const zr = this._zr = zrender.init(dom, {
	            renderer: opts.renderer || defaultRenderer,
	            devicePixelRatio: opts.devicePixelRatio,
	            width: opts.width,
	            height: opts.height,
	            useDirtyRect: opts.useDirtyRect == null ? defaultUseDirtyRect : opts.useDirtyRect
 });

初始化了this._scheduler = new Scheduler(this, api, dataProcessorFuncs, visualFuncs);,后续series渲染的时候会用到。
同时注册了渲染回调zr.animation.on('frame', this._onframe, this);

在分析setOption方法之前,有必要先说一下echarts的组件设计思想,我们示例里看到的绘制出来的图形,其实是很多组件构成的,echarts主要分成2类,一类是ComponentView,一类是ChartView。分别在src/chart下面和src/component下面。
先看下父类ComponentView(src/view/Component.ts)定义

class ComponentView {
    readonly group: ViewRootGroup;
    readonly uid: string;
    __model: ComponentModel;
    __alive: boolean;
    __id: string;

    constructor() {
        this.group = new Group();
        this.uid = componentUtil.getUID('viewComponent');
    }

    init(ecModel: GlobalModel, api: ExtensionAPI): void {}

    render(model: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {}

    dispose(ecModel: GlobalModel, api: ExtensionAPI): void {}

    updateView(model: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        
    }

    updateLayout(model: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        
    }

    updateVisual(model: ComponentModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        
    }

    
    blurSeries(seriesModels: SeriesModel[], ecModel: GlobalModel): void {
         
    }

    static registerClass: clazzUtil.ClassManager['registerClass'];
};

最主要的2个方法init和render是由各个组件子类来实现的。
ChartView(src/view/Chart.ts)

class ChartView {
    type: string;
    readonly group: ViewRootGroup;
    readonly uid: string;
    readonly renderTask: SeriesTask;
    ignoreLabelLineUpdate: boolean;
    __alive: boolean;
    __model: SeriesModel;
    __id: string;

    static protoInitialize = (function () {
        const proto = ChartView.prototype;
        proto.type = 'chart';
    })();

    constructor() {
        this.group = new Group();
        this.uid = componentUtil.getUID('viewChart');

        this.renderTask = createTask<SeriesTaskContext>({
            plan: renderTaskPlan,
            reset: renderTaskReset
        });
        this.renderTask.context = {view: this} as SeriesTaskContext;
    }

    init(ecModel: GlobalModel, api: ExtensionAPI): void {}

    render(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {}

    highlight(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        toggleHighlight(seriesModel.getData(), payload, 'emphasis');
    }

    downplay(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        toggleHighlight(seriesModel.getData(), payload, 'normal');
    }

    remove(ecModel: GlobalModel, api: ExtensionAPI): void {
        this.group.removeAll();
    }
    dispose(ecModel: GlobalModel, api: ExtensionAPI): void {}
    updateView(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        this.render(seriesModel, ecModel, api, payload);
    }

    
    updateLayout(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        this.render(seriesModel, ecModel, api, payload);
    }

    
    updateVisual(seriesModel: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload): void {
        this.render(seriesModel, ecModel, api, payload);
    }

    static markUpdateMethod(payload: Payload, methodName: keyof ChartView): void {
        inner(payload).updateMethod = methodName;
    }

    static registerClass: clazzUtil.ClassManager['registerClass'];
};

也是需要关注init和render方法,但是额外不一样的是构造函数里初始化了renderTask,这块在渲染数据的时候会用到。
下面再回到setOption方法,我们来看看setOption(src/core/echarts.ts)的实现

setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {
        if (this._disposed) {
            disposedWarning(this.id);
            return;
        }

        let silent;
        let replaceMerge;
        let transitionOpt: SetOptionTransitionOpt;
        if (isObject(notMerge)) {
            lazyUpdate = notMerge.lazyUpdate;
            silent = notMerge.silent;
            replaceMerge = notMerge.replaceMerge;
            transitionOpt = notMerge.transition;
            notMerge = notMerge.notMerge;
        }

        this[IN_MAIN_PROCESS_KEY] = true;

        if (!this._model || notMerge) {
            const optionManager = new OptionManager(this._api);
            const theme = this._theme;
            const ecModel = this._model = new GlobalModel();
            ecModel.scheduler = this._scheduler;
            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) {
            this[PENDING_UPDATE] = {
                silent: silent,
                updateParams: updateParams
            };
            this[IN_MAIN_PROCESS_KEY] = false;

            
            
            this.getZr().wakeUp();
        }
        else {
            prepare(this);

            updateMethods.update.call(this, null, updateParams);

            
            
            this._zr.flush();

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

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

初始化model,this._model.setOption(option as ECBasicOption, { replaceMerge }, optionPreprocessorFuncs);
prepare(this);的方法实现

prepare = function (ecIns: ECharts): void {
            const scheduler = ecIns._scheduler;

            scheduler.restorePipelines(ecIns._model);
            scheduler.prepareStageTasks();

            prepareView(ecIns, true);
            prepareView(ecIns, false);

            scheduler.plan();
};
restorePipelines(ecModel: GlobalModel): void {
        const scheduler = this;
        const pipelineMap = scheduler._pipelineMap = createHashMap();

        ecModel.eachSeries(function (seriesModel) {
            const progressive = seriesModel.getProgressive();
            const pipelineId = seriesModel.uid;

            pipelineMap.set(pipelineId, {
                id: pipelineId,
                head: null,
                tail: null,
                threshold: seriesModel.getProgressiveThreshold(),
                progressiveEnabled: progressive
                    && !(seriesModel.preventIncremental && seriesModel.preventIncremental()),
                blockIndex: -1,
                step: Math.round(progressive || 700),
                count: 0
            });

            scheduler._pipe(seriesModel, seriesModel.dataTask);
        });
    }

其中seriesModel的内容:

private _pipe(seriesModel: SeriesModel, task: GeneralTask) {
        const pipelineId = seriesModel.uid;
        const pipeline = this._pipelineMap.get(pipelineId);
        !pipeline.head && (pipeline.head = task);
        pipeline.tail && pipeline.tail.pipe(task);
        pipeline.tail = task;
        task.__idxInPipeline = pipeline.count++;
        task.__pipeline = pipeline;
  }

通过_pipe方法就将task加到了pipeline里,等待后续执行task进行渲染。
prepareView(ecIns, true);
prepareView(ecIns, false);
分别执行ComponentView和ChartView的init方法,具体怎么找到对应的clazz呢?

function doPrepare(model: ComponentModel): void {
                const requireNewView = model.__requireNewView;
                model.__requireNewView = false;
                const viewId = '_ec_' + model.id + '_' + model.type;
                let view = !requireNewView && viewMap[viewId];
                if (!view) {
                    const classType = parseClassType(model.type);
                    const Clazz = isComponent
                        ? (ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub)
                        : (
                                     (ChartView as ChartViewConstructor).getClass(classType.sub)
                        );

                    if (__DEV__) {
                        assert(Clazz, classType.sub + ' does not exist.');
                    }

                    view = new Clazz();
                    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
                );
     }
(ComponentView as ComponentViewConstructor).getClass(classType.main, classType.sub)

import * as clazzUtil from '../util/clazz';
export type ComponentViewConstructor = typeof ComponentView
    & clazzUtil.ExtendableConstructor
    & clazzUtil.ClassManager;

clazzUtil.enableClassExtend(ComponentView as ComponentViewConstructor);
clazzUtil.enableClassManagement(ComponentView as ComponentViewConstructor);

target.getClass = function (
        mainType: ComponentMainType,
        subType?: ComponentSubType,
        throwWhenNotFound?: boolean
    ): Constructor {
        let clz = storage[mainType];

        if (clz && (clz as SubclassContainer)[IS_CONTAINER]) {
            clz = subType ? (clz as SubclassContainer)[subType] : null;
        }

        if (throwWhenNotFound && !clz) {
            throw new Error(
                !subType
                    ? mainType + '.' + 'type should be specified.'
                    : 'Component ' + mainType + '.' + (subType || '') + ' is used but not imported.'
            );
        }

        return clz as Constructor;
 };
 
 target.registerClass = function (
        clz: Constructor
    ): Constructor {
        const componentFullType = (clz as any).type || clz.prototype.type;
        if (componentFullType) {
            checkClassType(componentFullType);
            clz.prototype.type = componentFullType;
            const componentTypeInfo = parseClassType(componentFullType);
            if (!componentTypeInfo.sub) {
                    storage[componentTypeInfo.main] = clz;
            }
            else if (componentTypeInfo.sub !== IS_CONTAINER) {
                const container = makeContainer(componentTypeInfo);
                container[componentTypeInfo.sub] = clz;
            }
        }
        return clz;
  };
  
  class TitleView extends ComponentView {

    static type = 'title' as const;
    type = TitleView.type;
prepareView(view: ChartView, model: SeriesModel, ecModel: GlobalModel, api: ExtensionAPI): void {
        const renderTask = view.renderTask;
        const context = renderTask.context;

        context.model = model;
        context.ecModel = ecModel;
        context.api = api;

        renderTask.__block = !view.incrementalPrepareRender;

        this._pipe(model, renderTask);
    }

而BarView不同的地方在,将renderTask加入了pipeline。
setOption里的prepare(this);执行完了,总结一下就是初始化Model,初始化对应的ComponentView和ChartView并将series对应的task加入pipeline,再来看下updateMethods.update.call(this, null, updateParams);

updateMethods = {
            update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void {
                const ecModel = this._model;
                const api = this._api;
                const zr = this._zr;
                const coordSysMgr = this._coordSysMgr;
                const scheduler = this._scheduler;
                
                if (!ecModel) {
                    return;
                }
                ecModel.setUpdatePayload(payload);
                scheduler.restoreData(ecModel, payload);
                scheduler.performSeriesTasks(ecModel);
                coordSysMgr.create(ecModel, api);
                scheduler.performDataProcessorTasks(ecModel, payload);
                updateStreamModes(this, ecModel);
                coordSysMgr.update(ecModel, api);
                clearColorPalette(ecModel);
                scheduler.performVisualTasks(ecModel, payload);
                
                render(this, ecModel, api, payload, updateParams);

                
                let backgroundColor = ecModel.get('backgroundColor') || 'transparent';
                const darkMode = ecModel.get('darkMode');

                
                if (!env.canvasSupported) {
                    const colorArr = colorTool.parse(backgroundColor as ColorString);
                    backgroundColor = colorTool.stringify(colorArr, 'rgb');
                    if (colorArr[3] === 0) {
                        backgroundColor = 'transparent';
                    }
                }
                else {
                    zr.setBackgroundColor(backgroundColor);

                    
                    if (darkMode != null && darkMode !== 'auto') {
                        zr.setDarkMode(darkMode);
                    }
                }

                lifecycle.trigger('afterupdate', ecModel, api);
            },

最主要的是render(this, ecModel, api, payload, updateParams);方法,

render = (
            ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
            updateParams: UpdateLifecycleParams
        ) => {

            renderComponents(ecIns, ecModel, api, payload, updateParams);

            each(ecIns._chartsViews, function (chart: ChartView) {
                chart.__alive = false;
            });

            renderSeries(ecIns, ecModel, api, payload, updateParams);

            
            each(ecIns._chartsViews, function (chart: ChartView) {
                if (!chart.__alive) {
                    chart.remove(ecModel, api);
                }
            });
     };

先来看下renderComponents(ecIns, ecModel, api, payload, updateParams);

renderComponents = (
            ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,
            updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[]
        ) => {
            each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) {
                const componentModel = componentView.__model;
                clearStates(componentModel, componentView);

                componentView.render(componentModel, ecModel, api, payload);

                updateZ(componentModel, componentView);

                updateStates(componentModel, componentView);
            });

    };

ecIns就是echarts实例,那针对这个示例都有哪些_componentsViews呢,

拿其中一个TitleView来细看一下

render(titleModel: TitleModel, ecModel: GlobalModel, api: ExtensionAPI) {
        this.group.removeAll();
        const group = this.group;

       
        const textEl = new graphic.Text({
            style: createTextStyle(textStyleModel, {
                text: titleModel.get('text'),
                fill: textStyleModel.getTextColor()
            }, {disableBox: true}),
            z2: 10
        });

        group.add(textEl);
        
    }
}

render方法实在太长了,我删掉了样式,子标题,背景,标题框等相关代码,只留下Text渲染相关的了。
可以看到,真正的渲染是调用了zrender的graphic.Text,并添加到了group里。
再来看renderSeries

renderSeries = (
            ecIns: ECharts,
            ecModel: GlobalModel,
            api: ExtensionAPI,
            payload: Payload | 'remain',
            updateParams: UpdateLifecycleParams,
            dirtyMap?: {[uid: string]: any}
        ) => {
            
            const scheduler = ecIns._scheduler;

            updateParams = extend(updateParams || {}, {
                updatedSeries: ecModel.getSeries()
            });

            let unfinished: boolean = false;
            ecModel.eachSeries(function (seriesModel) {
                const chartView = ecIns._chartsMap[seriesModel.__viewId];
                chartView.__alive = true;

                const renderTask = chartView.renderTask;
                 if (renderTask.perform(scheduler.getPerformArgs(renderTask))) {
                    unfinished = true;
                }

            });
     };

最重要的就是调用了renderTask.perform
Task(src/core/task.ts)的perform方法。方法比较长,我就不贴代码了,主干是调用了 this._doProgress(progress, start, end, modBy, modDataCount);

const progressMethodMap: {[method: string]: TaskResetCallbackReturn<SeriesTaskContext>} = {
    incrementalPrepareRender: {
        progress: function (params: StageHandlerProgressParams, context: SeriesTaskContext): void {
            context.view.incrementalRender(
                params, context.model, context.ecModel, context.api, context.payload
            );
        }
    },
    render: {
        forceFirstProgress: true,
        progress: function (params: StageHandlerProgressParams, context: SeriesTaskContext): void {
            context.view.render(
                context.model, context.ecModel, context.api, context.payload
            );
        }
    }
};

紧接着调用了progress方法,而progress方法里调用了view的render,以此示例的BarView举例

render(seriesModel: BarSeriesModel, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
        this._model = seriesModel;

        this._removeOnRenderedListener(api);

        this._updateDrawMode(seriesModel);

        const coordinateSystemType = seriesModel.get('coordinateSystem');

        if (coordinateSystemType === 'cartesian2d'
            || coordinateSystemType === 'polar'
        ) {
            this._isLargeDraw
                ? this._renderLarge(seriesModel, ecModel, api)
                : this._renderNormal(seriesModel, ecModel, api, payload);
        }
        else if (__DEV__) {
            warn('Only cartesian2d and polar supported for bar.');
        }
    }

根据是否是_isLargeDraw而分别调用不同的render,本示走的是_renderNormal方法。

private _renderNormal(
        seriesModel: BarSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: Payload
    ): void {
        const group = this.group;
        const data = seriesModel.getData();
        const oldData = this._data;
        data.diff(oldData)
            .add(function (dataIndex) {
                const itemModel = data.getItemModel<BarDataItemOption>(dataIndex);
                const layout = getLayout[coord.type](data, dataIndex, itemModel);
                const el = elementCreator[coord.type](
                    seriesModel,
                    data,
                    dataIndex,
                    layout,
                    isHorizontalOrRadial,
                    animationModel,
                    baseAxis.model,
                    false,
                    roundCap
                );
                group.add(el);
            })
            .execute();
        this._data = data;
    }
    
    const elementCreator: {
    [key in 'polar' | 'cartesian2d']: ElementCreator
} = {
    cartesian2d(
        seriesModel, data, newIndex, layout: RectLayout, isHorizontal,
        animationModel, axisModel, isUpdate, roundCap
    ) {
        const rect = new Rect({
            shape: extend({}, layout),
            z2: 1
        });
        (rect as any).__dataIndex = newIndex;

        rect.name = 'item';

        if (animationModel) {
            const rectShape = rect.shape;
            const animateProperty = isHorizontal ? 'height' : 'width' as 'width' | 'height';
            rectShape[animateProperty] = 0;
        }
        return rect;
    },
};

方法太长,我删掉了非主干代码,可以看到最终根据Model的data的diff来进行add,update,remove,最终还是调用了zrender的Rect来画出bar的样子。
最后贴一下渲染BarView的调用栈

渲染ComponentView比较直接,而渲染ChartView类型的就绕来绕去,主要原因是ChartView可能数据比较多,需要逐步分批渲染,以减少卡顿,保持每秒60帧的渲染,因为1000ms,每一帧不能超过16ms,才能保持流畅的渲染。
在setOption方法的最后调用了this._zr.flush();则直接将绘制渲染出来了,当然对于ChartView的绘制,可能还需要等到下一次渲染的时候才会显示出来。

private _onframe(): void {
        if (this._disposed) {
            return;
        }

        applyChangedStates(this);

        const scheduler = this._scheduler;

        if (this[PENDING_UPDATE]) {
            const silent = (this[PENDING_UPDATE] as any).silent;

            this[IN_MAIN_PROCESS_KEY] = true;

            prepare(this);
            updateMethods.update.call(this, null, this[PENDING_UPDATE].updateParams);

            this._zr.flush();

            this[IN_MAIN_PROCESS_KEY] = false;

            this[PENDING_UPDATE] = null;

            flushPendingActions.call(this, silent);

            triggerUpdatedEvent.call(this, silent);
        }
        
        else if (scheduler.unfinished) {
            
            let remainTime = TEST_FRAME_REMAIN_TIME;
            const ecModel = this._model;
            const api = this._api;
            scheduler.unfinished = false;
            do {
                const startTime = +new Date();

                scheduler.performSeriesTasks(ecModel);
                
                scheduler.performDataProcessorTasks(ecModel);

                updateStreamModes(this, ecModel);

                scheduler.performVisualTasks(ecModel);

                renderSeries(this, this._model, api, 'remain', {});

                remainTime -= (+new Date() - startTime);
            }
            while (remainTime > 0 && scheduler.unfinished);

            if (!scheduler.unfinished) {
                this._zr.flush();
            }
        }
   }

对于echarts的渲染分析就到这了,下次有机会我们自己动手写一个ChartView。