echart代码分析

1,374 阅读6分钟

一个例子

引入echart折线图

const echartsInstance = echarts.init(document.getElementById("main"))
const option = {
    xAxis: {
        type: 'category',
        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    },
    yAxis: {
        type: 'value'
    },
    series: [{
        data: [820, 932, 901, 934, 1290, 1330, 1320],
        type: 'line'
    }]
};
echartsInstance.setOption(option)

这样,一个简单的折线图绘制完成。

然而echart是如何绘制出一副图表的呢?这是这篇文章探讨的问题。

初始化

首先需要初始化echart的实例

// ./charts.ts

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

// ./core/echarts.ts

export function init(    dom: HTMLElement,    theme?: string | object,    opts?: {        renderer?: RendererType,        devicePixelRatio?: number,        width?: number,        height?: number,        locale?: string | LocaleOption    }): EChartsType {    const chart = new ECharts(dom, theme, opts); // 将Echarts实例化    chart.id = 'ec_' + idBase++;    instances[chart.id] = chart;    ...    return chart;}

看起来,初始化的核心工作集中在Echarts对象中,

class ECharts extends Eventful<ECEventDefinition> {
    id: string;
    group: string;
    private _zr: zrender.ZRenderType;
    private _dom: HTMLElement;
    private _model: GlobalModel;
    private _throttledZrFlush: zrender.ZRenderType extends {flush: infer R} ? R : never;
    ...

    constructor(
        dom: HTMLElement,
        theme?: string | ThemeOption,
        opts?: {
            locale?: string | LocaleOption,
            renderer?: RendererType,
            devicePixelRatio?: number,
            useDirtyRect?: boolean,
            width?: number,
            height?: number
        }
    ) {
        super(new ECEventProcessor());
        ...
        // 借助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
        });

        ...
        // 注册交互事件的回调函数,保证重绘
        zr.animation.on('frame', this._onframe, this);
        ...
    }
    ...
}

上述仅保留了构造函数的关键逻辑,

首先,基于zrender进行画布初始化(zrender.init),关于zrender的初始化的细节介绍,安排在zrender章节进行介绍。

接着注册了一些交互事件的回调函数,诸如zr.animation/ resize等。回调函数的核心功能是图表的重绘,细节这里不多介绍。

setOption

用户通过当前方法,能够将设置好的属性对象绑定至echarts实例上,实现图表的构建和首次渲染。具体逻辑如下

setOption<Opt extends ECBasicOption>(option: Opt, notMerge?: boolean | SetOptionOpts, lazyUpdate?: boolean): void {        ...        // 准备数据
        prepare(this);
        // 更新视图             updateMethods.update.call(this);        ...    }

上述片段仅保留了最核心的逻辑。

1. 准备数据

prepare方法会将图表中的子组件组合到一起,将组装好的图表对象存储到zr对象中来

// 准备数据
prepare(this);

 // 图表组建准备工作,引入各种图表 prepare = function (ecIns: ECharts): void {    ...    prepareView(ecIns, true);    ...};prepareView = function (ecIns: ECharts, isComponent: boolean): void {    ...    isComponent        ? ecModel.eachComponent(function (componentType, model) {            componentType !== 'series' && doPrepare(model);        })        : ecModel.eachSeries(doPrepare);    function doPrepare(model: ComponentModel): void {                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));            // 组合图表中的各个子组件            view = new Clazz();            view.init(ecModel, api);            viewMap[viewId] = view;            viewList.push(view as any);            zr.add(view.group);        }    }    ...};

2. 更新视图

通过调用update方法,实现图表视图的首次更新

updateMethods.update.call(this)

而update方法中,调用了render方法,它是渲染的核心方法

updateMethods = {    update(this: ECharts, payload: Payload, updateParams: UpdateLifecycleParams): void {        ...        render(this, ecModel, api, payload, updateParams);        ...    }}

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

render方法分为renderComponents(渲染Component)以及renderSeries(渲染series)两大部分

renderComponents方法中通过each遍历调用component下的render方法

renderComponents = (    ecIns: ECharts, ecModel: GlobalModel, api: ExtensionAPI, payload: Payload,    updateParams: UpdateLifecycleParams, dirtyList?: ComponentView[]) => {     // 渲染所有组件    each(dirtyList || ecIns._componentsViews, function (componentView: ComponentView) {        componentView.render(componentModel, ecModel, api, payload);    });};

renderSeries方法在echarts3.0中也是通过each遍历调用series下的render方法,在4.0版本之后,便将控制渲染的逻辑交给了scheduler调度器进行处理,通过调用charts中的reset方法进而调用series下的render方法,具体逻辑不再赘述。

在echart中,将通常所说的图表组件称为Series,如line折线图/ bar柱状图 / pie饼图**;**而将除了在此之外的相关组件称之为component, 如title 图标标题 / legend 图例组件 / AxisPointer 坐标轴指示器 / 坐标系 / 坐标轴等。

接下来分别介绍series和component。

第二部分 Component

针对各种图表相关组件,echart采用了MVC结构来拆分代码结构,其中Model用来管理组件数据, View用来渲染视图。

以图表标题组件(Title)为例,

使用时在option的对应字段,增加配置数据信息

title: { left: 'center',    text: '例子Title' }

title文件中Model部分通过extendComponentModel方法扩展自Component Model,重写了defaultOption属性,用于设置title的默认option

View部分通过extendComponentView方法扩展Component View,重写了render方法对title进行渲染

render(titleModel: TitleModel, ecModel: GlobalModel, api: ExtensionAPI) {        // 新增text元素    const textStyleModel = titleModel.getModel('textStyle');    const textEl = new graphic.Text({        style: createTextStyle(textStyleModel, {            text: titleModel.get('text'),            fill: textStyleModel.getTextColor()        }, {disableBox: true}),        z2: 10    });    group.add(textEl);    // 新增样式    const alignStyle = {        align: textAlign,        verticalAlign: textVerticalAlign    };    textEl.setStyle(alignStyle);    // 新增背景    groupRect = group.getBoundingRect();    const rect = new graphic.Rect({        shape: {...},        style: style,        subPixelOptimize: true,        silent: true    });    group.add(rect);}

title文本渲染主要是通过zrender graphic中的Text进行渲染的,通过zrender Style中定义的setStyle方法对元素进行样式设定。

第三部分 Series

与component的组件结构类似,Series组建也拆分为model和view两部分

以折线图Line组件为例

LineSeries

LineSeries通过extend方法扩展自Series Model,重写了defaultOption属性以及getInitialData方法。

Lineview

LineView通过extend方法扩展自Chart View,重写了init、render、highlight及downplay等方法

render(seriesModel: LineSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {    ...
    // 通过zrender Polyline绘制折线图    polyline = this._newPolyline(points, coordSys, hasAnimation);
    if (isAreaChart && !polygon) {        // 通过zrender Polygon绘制折线区域        polygon = this._newPolygon(            points, stackedOnPoints        );    }    else if (polygon && !isAreaChart) {        // If areaStyle is removed        lineGroup.remove(polygon);        polygon = this._polygon = null;    }    ...    if (polygon) {        ...        polygon.useStyle(zrUtil.defaults(            areaStyleModel.getAreaStyle(),            {                fill: visualColor,                opacity: 0.7,                lineJoin: 'bevel' as CanvasLineJoin,                decal: data.getVisual('style').decal            }        ));        polygon.setShape({            smooth,            stackedOnSmooth,            smoothMonotone,            connectNulls        });        ...    }    ...}

其中,基础折线、折线区域的绘制,依赖了zrender的基础绘图api的支持。

以上就是echart的核心绘图逻辑。(事件机制等重要但非核心的模块暂时未做介绍)

可以看到,无论是图表的初始化(zrender.init)、还是具体组件的渲染(new graphic.text)等,均依赖了一个叫做zrender的底层绘图库,所有的绘图细节都被封壮在其中。接下来,我们来分析下zrender中的结构。

Ps:Zrender基础绘图库

zrender是基于canvas的一个绘图库,整体架构采用MVC

- Storage(M):图形数据的CRUD

- Painter(V):对canvas元素的渲染和更新进行管理

- Handler(C):实现事件交互处理,对canvas元素实现类dom的模拟封装

在基础架构下,首先关注下提供的一系列基础形状组件。目录在./src/graphic/shape

以Circle.ts为例,

class Circle extends Path<CircleProps> {    shape: CircleShape    constructor(opts?: CircleProps) {        super(opts);    }    buildPath(ctx: CanvasRenderingContext2D, shape: CircleShape, inBundle: boolean) {        if (inBundle) {            ctx.moveTo(shape.cx + shape.r, shape.cy);        }        ctx.arc(shape.cx, shape.cy, shape.r, 0, Math.PI * 2);    }};

Circle类继承自基础图形类Path,重写了buildPath方法,完成了canvas具体图形的绘制。

另外

Path类定义了基础图形的接口方法,如buildPath等;并实现了部分通用方法,如contain()等。Path类继承自Displayable类。

技术方案特点

1.模块拆分清晰

    renderer提升为接口,具体渲染器可扩展canvas、svg等。 ./src/renderer/{renderer}    默认canvas渲染器会引入zrender/src/canvas/Painter

    具体canvas的基础图形渲染,以及基础的绘图api的能力独立拆分出,封装为zrender库。

2. 扩展性强

    tooltip支持canvas渲染、dom渲染

3.渲染性能强 

   canvas绘图,支持同步渲染、异步批量渲染。