echarts源码解读《二》:echarts源码概要分析

继解读完zrender源码之后,我们便开始进入echarts源码解读,echarts是在zrender基础上进行开发的,在这篇博文中,我将会分析在使用echarts时,echarts的渲染过程,接下来系列博文中我将从echarts的Component以及View进行解读echarts如何实现图表绘制。

希望能够一起努力学习呀!!加油!!

前言

我们首先来看一个简单的echarts折线图Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

效果图如下:

折线图

上述代码,使用echarts实现了简单的折线图,那么echarts是如何根据用户设定的option进行图表绘制的呢?这也是我们今天需要探讨的问题。

Echarts解读

echarts.js,定义了我们使用echarts时直接调用的init、setOption等方法。

初始化echarts实例

在echarts.js文件中通过export init方法提供用户初始化echarts实例的接口,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
export function init(dom, theme, opts) {
if (__DEV__) {
// Check version
if ((zrender.version.replace('.', '') - 0) < (dependencies.zrender.replace('.', '') - 0)) {
throw new Error(
'zrender/src ' + zrender.version
+ ' is too old for ECharts ' + version
+ '. Current version need ZRender '
+ dependencies.zrender + '+'
);
}

if (!dom) {
throw new Error('Initialize failed: invalid dom.');
}
}
// 判断该DOM结构是否已经存在绑定的echarts实例
var existInstance = getInstanceByDom(dom);
if (existInstance) {
if (__DEV__) {
console.warn('There is a chart instance already initialized on the dom.');
}
return existInstance;
}

// 需要DOM结构设定特定的width以及height值
if (__DEV__) {
if (zrUtil.isDom(dom)
&& dom.nodeName.toUpperCase() !== 'CANVAS'
&& (
(!dom.clientWidth && (!opts || opts.width == null))
|| (!dom.clientHeight && (!opts || opts.height == null))
)
) {
console.warn('Can\'t get dom width or height');
}
}
// 新建Echarts实例
var chart = new ECharts(dom, theme, opts);
chart.id = 'ec_' + idBase++;
instances[chart.id] = chart;

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

enableConnect(chart);

return chart;
}

##Echarts对象

Echarts对象属性包括:

  • this._zr(Zrender实例)
  • this._dom(DOM)
  • this._theme(主题)
  • this._chartsViews&this._chartsMap(保存View数据)
  • this._componentsViews&this._componentsMap(保存component数据)
  • this._api(对外API数据)
  • this._model(该echarts实例信息)

Echarts对象方法包括:

  • getDom():获取当前实例所挂载的DOM信息
  • getZr():获取zrender实例
  • setOption():设置echarts option(后续将着重讲解)
  • setTheme():设置echarts主题
  • getModel():获取echarts实例
  • getOption():获取option信息
  • getWidth():this._zr.getWidth() => 通过zrender实例返回zrender中painter画布的大小
  • getHeight():同getWidth()
  • getRenderedCanvas():this._zr.painter.getRenderedCanvas() => 获取画布中渲染的canvas元素
  • dispatchAction(): 触发action(后续将着重讲解)
  • dispose():销毁echarts实例

setOption

setOption()方法为用户使用echarts最直接的接口方法,用户通过setOption方法能够将设置好的option绑定至echarts图表中进行显示,那么echarts内部又是如何根据用户设定的option进行图表渲染的呢

首先,我们来分析Echarts中最基本的元素Model、OptionManager以及全局Model

OptionManager

OptionManager为option管理器,对option进行了管理与处理,定义了setOption、mountOption等方法,包括对原生option的处理,原生option格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var option = {
baseOption: {
title: {...},
legend: {...},
series: [
{data: [...]},
{data: [...]},
...
]
},
timeline: {...},
options: [
{title: {...}, series: {data: [...]}},
{title: {...}, series: {data: [...]}},
...
],
media: [
{
query: {maxWidth: 320},
option: {series: {x: 20}, visualMap: {show: false}}
},
{
query: {minWidth: 320, maxWidth: 720},
option: {series: {x: 500}, visualMap: {show: true}}
},
{
option: {series: {x: 1200}, visualMap: {show: true}}
}
]
};

Model

Model是Echarts中最基本的元素,其定义了mergeOption等方法,混合了LineStyle、AraeStyle、ItemStyle以及TextStyle。是Component(后续介绍)以及GlobalModel等元素的基类。

GlobalModel

GlobalModel扩展自Model,GlobalModel中定义了init方法,要求在初始化GloblaModel对象实例时需要传递OptionManager实例作为构造方法的参数,GlobalModel中定义了查找component、series(Views),管理option等方法。

  • setOption():通过optionManager.setOption()实现
  • resetOption():通过optionManager.mountOption()实现
  • 重写mergeOption()
  • getComponent():通过component的mainType获取component
  • queryComponents():除mainType外,将id、index、name也作为查询component的条件进行component的检索
  • findComponents():与queryComponents类似,但echarts源码中有注释此方法更方便于内部使用
  • eachComponent():遍历component
  • getSeriesByName():通过series name获取series
  • getSeriesByIndex():通过seriesIndex获取series
  • getSeriesByType():通过series subType获取series
  • getSeries():获取series数据
  • eachSeries():遍历series
  • filterSeries():根据条件过滤series

setOption调用后echarts操作流程

回归setOption方法的调用,echarts.js的setOption方法的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
echartsProto.setOption = function (option, notMerge, lazyUpdate) {
if (__DEV__) {
assert(!this[IN_MAIN_PROCESS], '`setOption` should not be called during main process.');
}

var silent;
if (isObject(notMerge)) {
lazyUpdate = notMerge.lazyUpdate;
silent = notMerge.silent;
notMerge = notMerge.notMerge;
}

this[IN_MAIN_PROCESS] = true;

if (!this._model || notMerge) {
var optionManager = new OptionManager(this._api);
var theme = this._theme;
// 初始化model
var ecModel = this._model = new GlobalModel(null, null, theme, optionManager);
ecModel.scheduler = this._scheduler;
ecModel.init(null, null, theme, optionManager);
}
// 调用GlobalModel中的setOption方法
this._model.setOption(option, optionPreprocessorFuncs);

if (lazyUpdate) {
this[OPTION_UPDATED] = {silent: silent};
this[IN_MAIN_PROCESS] = false;
}
else {
// 准备数据
prepare(this);
// 更新视图
updateMethods.update.call(this);
...
}
};

在调用globalModel.setOption方法之后,echarts通过调用

1
updateMethods.update.call(this);

进行视图更新。

update()方法中调用了render方法进行视图渲染,render方法主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function render(ecIns, ecModel, api, payload) {

renderComponents(ecIns, ecModel, api, payload);

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

renderSeries(ecIns, ecModel, api, payload);

// Remove groups of unrendered charts
each(ecIns._chartsViews, function (chart) {
if (!chart.__alive) {
chart.remove(ecModel, api);
}
});
}

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

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

renderSeries方法在echarts3.0中也是通过each遍历调用series下的render方法,在4.0版本之后,便将控制渲染的逻辑交给了scheduler调度器进行处理,通过调用charts中的reset方法进而调用series下的render方法,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function renderTaskReset(context) {
var seriesModel = context.model;
var ecModel = context.ecModel;
var api = context.api;
var payload = context.payload;
// ???! remove updateView updateVisual
var progressiveRender = seriesModel.pipelineContext.progressiveRender;
var view = context.view;

var updateMethod = payload && inner(payload).updateMethod;
var methodName = progressiveRender
? 'incrementalPrepareRender'
: (updateMethod && view[updateMethod])
? updateMethod
// `appendData` is also supported when data amount
// is less than progressive threshold.
: 'render';

if (methodName !== 'render') {
view[methodName](seriesModel, ecModel, api, payload);
}

return progressMethodMap[methodName];
}
var progressMethodMap = {
...
render: {
forceFirstProgress: true,
progress: function (params, context) {
context.view.render(
context.model, context.ecModel, context.api, context.payload
);
}
}
};

dispatchAction

我们在使用echarts的过程中,经常会需要使用echarts对外提供的一些api,如:

1
2
3
4
5
echartsInstance.dispatchAction({
type: 'updateAxisPointer',
x: 20,
y: 30
});

那么echarts是怎么对这些api进行处理的呢

能够通过dispatchAction使用的api,首先必须通过registerAction注册api,举updateAxisPointer的🌰,注册dataZoom api的主要代码如下:

1
2
3
4
5
echarts.registerAction({
type: 'updateAxisPointer',
event: 'updateAxisPointer',
update: ':updateAxisPointer'
}, axisTrigger);

registerAction主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function registerAction(actionInfo, eventName, action) {
// 参数处理:将传递过来的trigger作为action进行保存
if (typeof eventName === 'function') {
action = eventName;
eventName = '';
}
var actionType = isObject(actionInfo)
? actionInfo.type
: ([actionInfo, actionInfo = {
event: eventName
}][0]);

// Event name is all lowercase
actionInfo.event = (actionInfo.event || actionType).toLowerCase();
eventName = actionInfo.event;

// Validate action type and event name.
assert(ACTION_REG.test(actionType) && ACTION_REG.test(eventName));
// 将action信息保存至actions
if (!actions[actionType]) {
actions[actionType] = {action: action, actionInfo: actionInfo};
}
eventActionMap[eventName] = actionType;
}

还是看updateAxisPointer这个🌰:

1
2
3
4
5
echartsInstance.dispatchAction({
type: 'updateAxisPointer',
x: 20,
y: 30
});

dispatchAction的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
echartsProto.dispatchAction = function (payload, opt) {
if (!isObject(opt)) {
opt = {silent: !!opt};
}

if (!actions[payload.type]) {
return;
}

// Avoid dispatch action before setOption. Especially in `connect`.
if (!this._model) {
return;
}

// May dispatchAction in rendering procedure
if (this[IN_MAIN_PROCESS]) {
this._pendingActions.push(payload);
return;
}

// 调用doDispatchAction
doDispatchAction.call(this, payload, opt.silent);
...
};

function doDispatchAction(payload, silent) {
var payloadType = payload.type;
var escapeConnect = payload.escapeConnect;
// 根据paoload中的type信息,获取actions对应的action数据
var actionWrap = actions[payloadType];
var actionInfo = actionWrap.actionInfo;

var cptType = (actionInfo.update || 'update').split(':');
var updateMethod = cptType.pop();
cptType = cptType[0] != null && parseClassType(cptType[0]);

this[IN_MAIN_PROCESS] = true;

var payloads = [payload];
var batched = false;
// Batch action
if (payload.batch) {
batched = true;
payloads = zrUtil.map(payload.batch, function (item) {
item = zrUtil.defaults(zrUtil.extend({}, item), payload);
item.batch = null;
return item;
});
}

var eventObjBatch = [];
var eventObj;
var isHighDown = payloadType === 'highlight' || payloadType === 'downplay';

each(payloads, function (batchItem) {
// Action can specify the event by return it.
// 触发action trigger
eventObj = actionWrap.action(batchItem, this._model, this._api);
// Emit event outside
eventObj = eventObj || zrUtil.extend({}, batchItem);
// Convert type to eventType
eventObj.type = actionInfo.event || eventObj.type;
eventObjBatch.push(eventObj);

// light update does not perform data process, layout and visual.
if (isHighDown) {
// method, payload, mainType, subType
updateDirectly(this, updateMethod, batchItem, 'series');
}
else if (cptType) {
updateDirectly(this, updateMethod, batchItem, cptType.main, cptType.sub);
}
}, this);
...
}

总结

博文到这里就差不多结束了,在此博文中,我对echarts渲染视图的过程进行了简单的分析,并对setOption以及dispatchAction方法的实现过程进行了大致的分析。

接下来的文章将会深入Component以及Series中,解读echarts是如何使用zrender graphic渲染图表的,希望能帮助到大家~一起加油!!

本文标题:echarts源码解读《二》:echarts源码概要分析

文章作者:萌萌哒的邱邱邱邱

发布时间:2019年05月21日 - 10:05

最后更新:2019年05月22日 - 15:05

原始链接:https://qiuruolin.github.io/2019/05/21/echarts-2/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------
感谢您的支持