漫漫技术路

  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

ECharts 3.0源码简要分析1-总体架构

发表于 2017-03-09 | 更新于 2018-11-30 | 分类于 前端技术

百度的Echarts 3.0作为前端领域可视化重要的开源库,是我们在日常工作生活中经常使用的,所以有必要一起来了解下Echarts的源码。我打算用一个系列介绍下Echarts 3.x的使用和源码,一些demo和没有在博客中介绍的源码请进我的github仓库。

https://github.com/zrysmt/echarts3/tree/master/echarts

本博文Echarts版本基于3.3.2。

Echarts的源码是在zrender的基础上封装的,所以要看明白echarts源码须要先了解zrender的源码,不过为了本博文的独立可读性,这里也会将用到的zrender源码简单说明。如果要了解zrender具体的源码,这里给出了zrender源码解读博客和源码注释仓库。

github仓库:https://github.com/zrysmt/echarts3/tree/master/zrender

ECharts 3.0底层zrender 3.x源码分析1-总体架构
ECharts 3.0底层zrender 3.x源码分析2-Painter(V层)
ECharts 3.0底层zrender 3.x源码分析3-Handler(C层)

1.源码结构和打包

1.1 源码打包

源码使用webpack打包,查看文件webpack.config.js可知,将echarts源码编译成三个版本,分别为常用版本,精简版本,完整版本,分别对应webpack入口文件为index.common.js、index.simple.js、index.js。

注:三个文件引用的都是lib文件下的文件,执行下面一步提示的命令npm insall后就可以得到lib文件夹,它里面的文件和src文件夹中的文件主要内容是相同的,不同之处在于:前者文件是通过类似CMD的模式打包的,后者文件是通过webpack进行打包的。我们在下面就分析src文件夹下的源码。注释也在其中。

执行命令顺序为

1
2
3
npm install  //安装所有依赖包
webpack //打包
webpack -p //打成压缩包(.min.js)

最后生成的文件在dist文件夹下。

1.2 源码结构

首先我们要明白两个重要的概念components和charts:charts是指各种类型的图表,例如line,bar,pie等,在配置项中指的是series对应的配置;components组件是在配置项中除了serie的其余项,例如title,legend,toobox等。

源码的重要目录及说明如下(注:dist为编译后生成的文件夹)

  • extension (扩展中使用)
  • lib (源码中没有,执行webpack编译后才存在)
  • map (世界地图,中国地图及中国各个省份地图的js和json两种格式的文件)
  • src (核心源码)
  • test (示例demo)
  • theme (主题)

2 渲染情况

完整的例子代码戳我。

最外层是id为main的div,是我们自己写的用来渲染echarts图表的。
echarts渲染了两个div,一个div用来渲染主要的图表的,div里面嵌套一个canvas标签,
第二个div是为了显示hover层信息的。

3.入口echarts.js

位置:src/echarts.js。

大体的结构是一个构造函数(ECharts),原型上(ECharts.prototype)多个方法,一个echarts对象(包括对象上的属性和方法)。

和zrender一样,使用init方法进行初始化。

3.1 init方法

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
echarts.init = function(dom, theme, opts) {
if (__DEV__) {//是否是debug模式
//... //错误判断这部分内容省略
}

var chart = new ECharts(dom, theme, opts);//实例化ECharts
chart.id = 'ec_' + idBase++;//chart实例的id号,唯一,逐一递增
instances[chart.id] = chart;//唯一instance(实例)对象

dom.setAttribute &&
dom.setAttribute(DOM_ATTRIBUTE_KEY, chart.id);//为外层dom设置了一个属性,属性值等于chart.id

enableConnect(chart);//按照顺序更新状态,一共三个状态
/*var STATUS_PENDING = 0;
var STATUS_UPDATING = 1;
var STATUS_UPDATED = 2;*/

return chart;
};
  • if (__DEV__)验证是否是debug模式,如果是就会有错误提示(错误判断这部分内容省略),否者就是生产模式,没有错误提示。
  • 参数说明
1
2
3
4
5
6
7
8
9
10
11
/**
* @param {HTMLDomElement} dom 实例容器,一般是一个具有高宽的div元素
* @param {Object} [theme] 主题(说明见下面)
* @param {Object} opts 配置属性,下面几个属性
* @param {number} [opts.devicePixelRatio] Use window.devicePixelRatio by default
* @param {string} [opts.renderer] Currently only 'canvas' is supported.
* @param {number} [opts.width] Use clientWidth of the input `dom` by default.
* Can be 'auto' (the same as null/undefined)
* @param {number} [opts.height] Use clientHeight of the input `dom` by default.
* Can be 'auto' (the same as null/undefined)
*/
  • 主题theme
1
2
3
4
5
6
7
8
/*theme主题,可以在官网下载(http://echarts.baidu.com/download-theme.html),或者自己构建
* 使用:
* <script src="theme/vintage.js"></script>
* <script>
* // 第二个参数可以指定前面引入的主题
* var chart = echarts.init(document.getElementById('main'), 'vintage');
* </script>
*/

使用:

1
2
var chart = echarts.init(document.getElementById('main'), null, {
renderer: 'canvas'});

3.2 构造函数

构造函数里面是属性的初始化和zrender的初始化(this._zr)。

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
function ECharts(dom, theme, opts) {
opts = opts || {};
if (typeof theme === 'string') {
theme = themeStorage[theme];
}
this.id;
this.group;
this._dom = dom;
var zr = this._zr = zrender.init(dom, {
renderer: opts.renderer || 'canvas',
devicePixelRatio: opts.devicePixelRatio,
width: opts.width,
height: opts.height
});//构造函数第三个参数使用的zrender处理的

this._throttledZrFlush = throttle.throttle(zrUtil.bind(zr.flush, zr), 17);
this._theme = zrUtil.clone(theme);
this._chartsViews = [];//存储所有的charts,为后面便利该变量渲染之
this._chartsMap = {};
this._componentsViews = [];//存储配置项组件的属性,为后面便利该变量渲染之
this._componentsMap = {};
this._api = new ExtensionAPI(this);
//this._api是有'getDom', 'getZr', 'getWidth', 'getHeight', 'dispatchAction', 'isDisposed',
//'on', 'off', 'getDataURL', 'getConnectedDataURL', 'getModel', 'getOption'方法的对象
this._coordSysMgr = new CoordinateSystemManager();
Eventful.call(this);
this._messageCenter = new MessageCenter();
this._initEvents();//初始化鼠标事件
this.resize = zrUtil.bind(this.resize, this);

this._pendingActions = [];
function prioritySortFunc(a, b) {
return a.prio - b.prio;
}
timsort(visualFuncs, prioritySortFunc);
timsort(dataProcessorFuncs, prioritySortFunc);
zr.animation.on('frame', this._onframe, this);
}

3.3 setOption

首先我们来看下使用api的情况,我们在前面已经说过使用init方法初始化echarts了,接下来只需要配置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
31
32
33
34
35
36
37
38
39
chart.setOption({
backgroundColor: '#eee',
title: {
text: '我是柱状图',
padding: 20
},
legend: {
inactiveColor: '#abc',
borderWidth: 1,
data: [{name: 'bar'}, 'bar2', '\n', 'bar3', 'bar4'],
align: 'left',
tooltip: {show: true }
},
toolbox: {
top: 25,
feature: {
magicType: { type: ['line', 'bar', 'stack', 'tiled']},
dataView: {},
saveAsImage: {pixelRatio: 2}
},
iconStyle: {
emphasis: {textPosition: 'top'}
}
},
tooltip: {},
xAxis: { //...
},
yAxis: { //...
},
series: [{
name: 'bar', type: 'bar', stack: 'one',
itemStyle: itemStyle, data: data1
}, {
name: 'bar2', type: 'bar', stack: 'one',
itemStyle: itemStyle, data: data2
}, { //... ...
}, { //... ...
}]
});

源码的主要部分列下来:

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
/**
* @param {Object} option 配置项
* @param {boolean} notMerge 可选,是否不跟之前设置的option进行合并,默认为false,即合并。
* @param {boolean} [lazyUpdate=false] Useful when setOption frequently.
* //可选,在设置完option后是否不立即更新图表,默认为false,即立即更新
*/
echartsProto.setOption = function(option, notMerge, lazyUpdate) {

this[IN_MAIN_PROCESS] = true;

if (!this._model || notMerge) { //不和之前的option合并
var optionManager = new OptionManager(this._api); //option配置管理
var theme = this._theme;
var ecModel = this._model = new GlobalModel(null, null, theme, optionManager);
ecModel.init(null, null, theme, optionManager);
//不合并的时候会重绘,option为最后一次使用setOption方法的参数
}


this.__lastOnlyGraphic = !!(option && option.graphic); //是否设置了graphic属性
//graphic 是原生图形元素组件。可以支持的图形元素包括:image, text, circle, sector,
//ring, polygon, polyline, rect, line, bezierCurve, arc, group,
//http://echarts.baidu.com/option.html#graphic
zrUtil.each(option, function(o, mainType) {
mainType !== 'graphic' && (this.__lastOnlyGraphic = false);
}, this);

//setOption之前先执行的函数列表optionPreprocessorFuncs
this._model.setOption(option, optionPreprocessorFuncs);

if (lazyUpdate) { //为true,不立刻更新
this[OPTION_UPDATED] = true;
} else {
updateMethods.prepareAndUpdate.call(this); //准备更新
// Ensure zr refresh sychronously, and then pixel in canvas can be
// fetched after `setOption`.
this._zr.flush(); //调用zrender中的方法,立即刷新
this[OPTION_UPDATED] = false;
}

this[IN_MAIN_PROCESS] = false;

flushPendingActions.call(this, false);
};

说明: 这里setOption调用的顺序是这样的echarts.setOption==>GlobalModel.setOption(GlobalModel.js)==>OptionManager.setOption(OptionMManager.js)

其中有两个关键的方法:prepareAndUpdate和flush,分别用来准备刷新和刷新,渲染图表,下面我们来一步一步看prepareAndUpdate方法。

3.4 doRender方法

接着上面的prepareAndUpdate方法看准备渲染图表视图的执行顺序:updateMethods.prepareAndUpdate==>updateMethods.update==>doRender==>render。
在doRneder函数中渲染所有components和charts,render方法分别对应在各个components和charts中有具体的实现。

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
function doRender(ecModel, payload) {
var api = this._api;
// Render all components 渲染所有的配置组件,例如title,grid,toolbox,tooltip等
each(this._componentsViews, function(componentView) {
var componentModel = componentView.__model;
console.info("componentModel:", componentModel);
componentView.render(componentModel, ecModel, api, payload);
//在componentModal文件夹下调用相应的render方法
updateZ(componentModel, componentView);
}, this);

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

// Render all charts 渲染所有的charts
ecModel.eachSeries(function(seriesModel, idx) {
var chartView = this._chartsMap[seriesModel.__viewId]; //this._chartsMap
chartView.__alive = true;

chartView.render(seriesModel, ecModel, api, payload);
chartView.group.silent = !!seriesModel.get('silent');

updateZ(seriesModel, chartView);
updateProgressiveAndBlend(seriesModel, chartView);

}, this);

// If use hover layer 如果使用hover,更新hover层
updateHoverLayerStatus(this._zr, ecModel);

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

4.关于option的处理

3.3部分已经说过使用setOption处理配置项options,这里介绍下源码里面是怎样管理配置项的。主要源码在echarts/model/Model(以下简称Model),echarts/model/Global(以下简称GlobalModel,继承Model),echarts/model/OptionManager(以下简称OptionManager)

1
this._model.setOption(option, optionPreprocessorFuncs);

注意这里面有个对象this._model用来存储配置项options的。

  • Model模块是一些基本的方法,主要的方法就是get,getModel通过options的对象名获取对象值。还混合了lineStyle,areaStyle,textStyle,itemStyle方法用来管理与线,文本,项目有关的options属性。

  • GlobalModel继承Model,暴露Model的方法,再封装一些自己独有的方法。

  • OptionManager是用来管理options配置项的,有重要的setOption方法,mergeOption方法(私有方法,合并options),parseRawOption方法(私有方法,解析options)

5.component组件和charts图表

component组件和charts图表均有render方法,这是我们来重点探究的方法。

5.1 component组件

component组件和配置项的属性一一对应,对于复杂点的配置项,组件文件夹下的管理方式是按照MVC方式的,如legend文件夹下有基本的LegendModel.js(M),LegendView(V),LegendAction(C),与其他的组件一样,还可能有其他的一些js文件。

先看一个比较简单的例子,如title.js,我们来分析下它的render方法。

  • 首先是使用extendComponentModel,相当于Model层,用来配置一些默认的配置项(有的复杂点的会把这些单独拆分开一个Model文件)。
  • 然后是extendComponentView,相当于View层,render方法就在其中(有的复杂点的会把这些单独拆分开一个View文件)。

在render方法中首先当然是获取到配置options对象的内容。通过titleModel.getModel(path)(每个组件都会有一个对应的Model名称)获取对应的属性path。
然后调用new graphic.Text()去调用zrender里面的方法,渲染到canvas上,具体的实现参考上面给出的zrender源码分析内容。

5.2 charts图表

在charts文件夹下是各种类型的图表,包含line,bar,pie,map等,每种类型的文件夹下都有下面的几个文件结尾的js文件。

  • **Series.js 继承src/modal/series.js中的基础方法,用来管理配置中的series属性,还提供一些默认的配置defaultOption;
  • **Veiw.js 继承至src/view/Chart.js(这里面相当于接口,没有具体实现方法),主要方法是render,渲染视图
    render方法调用的是zrender.js中的内容,如lineView.js调用的是new graphic.Rect.

6.事件

示例中的使用:

1
2
3
chart.on('click', function(params) {
console.log(params);
});

关键源码:

1
2
3
4
5
6
7
8
9
10
function createRegisterEventWithLowercaseName(method) {
return function (eventName, handler, context) {
// Event name is all lowercase
eventName = eventName && eventName.toLowerCase();
Eventful.prototype[method].call(this, eventName, handler, context);
};
}
echartsProto.on = createRegisterEventWithLowercaseName('on');
echartsProto.off = createRegisterEventWithLowercaseName('off');
echartsProto.one = createRegisterEventWithLowercaseName('one');

Eventful使用的是zrender中的(zrender/mixin/Eventful),是事件扩展,包括on,off,one,trigger等方法。

我们知道canvas API没有提供监听每个元素的机制,这就需要一些处理。处理的思路是:监听事件的作用坐标(如点击时候的坐标),判断在哪个绘制元素的范围中,如果在某个元素中,这个元素就监听该事件。具体的思路可以查看HTML5 Canvas绘制的图形的事件处理。

参考阅读:

  • echarts 3中文官网
  • echarts 3 github网址
  • ECharts 3.0底层zrender 3.x源码分析1-总体架构
  • ECharts 3.0底层zrender 3.x源码分析2-Painter(V层)
  • ECharts 3.0底层zrender 3.x源码分析3-Handler(C层)

ECharts 3.0底层zrender 3.x源码分析2-Painter(V层)

发表于 2017-01-11 | 更新于 2018-11-30 | 分类于 前端技术

上一篇介绍了zrender的总体结构,这一篇我们就详细介绍View层–Painter(Painter.js)。

一些demo和没有在博客中介绍的源码请进我的github仓库。

https://github.com/zrysmt/echarts3/tree/master/zrender

Painter利用canvas负责真正的绘图操作。

  • 1.负责canvas及其周边DOM元素的创建与处理
  • 2.负责调用各个Shape(预定义好的)进行绘制
  • 3.提供基本的操作方法,渲染(render)、刷新(refresh)、尺寸变化(resize)、擦除(clear)等

1.渲染结构分析

两个例子都是渲染到div上。

1
<div id="main" style="width:1000px;height:600px;margin:0;"></div>

zrender 3.x版本渲染结果(demo/demo1/demo3-chart.html)

我们可以看到渲染结果都会新建一层div(从下面的分析我们可以得到这个div就是_domRoot),里面嵌套canvas。如果有使用addHover(有hover层,data-zr-dom-id=”zr_100000”)的话,hover层会单独列一个canvas画布。

1
2
3
4
5
6
7
8
9
10
11
sector.on('mouseover', function() {
zr.addHover(this, {
stroke: 'yellow',
lineWidth: 10,
opacity: 1
});
zr.refresh();
});
sector.on('mouseout', function() {
zr.removeHover(this);
});

1
2
3
4
5
6
<div id="main" style="width: 1000px; height: 600px; margin: 0px; -webkit-tap-highlight-color: transparent; user-select: none;">
<div style="position: relative; overflow: hidden; width: 1000px; height: 600px; padding: 0px; margin: 0px; border-width: 0px; cursor: default;">
<canvas width="1000" height="600" data-zr-dom-id="zr_0" style="position: absolute; left: 0px; top: 0px; width: 1000px; height: 600px; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); padding: 0px; margin: 0px; border-width: 0px;"></canvas>
<canvas width="1000" height="600" data-zr-dom-id="zr_100000" style="position: absolute; left: 0px; top: 0px; width: 1000px; height: 600px; user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); padding: 0px; margin: 0px; border-width: 0px;"></canvas>
</div>
</div>

2.构造函数

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
var Painter = function (root, storage, opts) {
// In node environment using node-canvas
var singleCanvas = !root.nodeName // In node ?
|| root.nodeName.toUpperCase() === 'CANVAS';
this._opts = opts = util.extend({}, opts || {});
this.dpr = opts.devicePixelRatio || config.devicePixelRatio;
this._singleCanvas = singleCanvas;
/**
* 绘图容器
* @type {HTMLElement}
*/
this.root = root;
var rootStyle = root.style;
if (rootStyle) {
rootStyle['-webkit-tap-highlight-color'] = 'transparent';
rootStyle['-webkit-user-select'] =
rootStyle['user-select'] =
rootStyle['-webkit-touch-callout'] = 'none';
root.innerHTML = '';
}
/**
* @type {module:zrender/Storage}
*/
this.storage = storage;
/**
* 存储图层画布,这个变量很重要
* @type {Array.<number>}
* @private
*/
var zlevelList = this._zlevelList = [];

/** 图层
* @type {Object.<string, module:zrender/Layer>}
* @private
*/
var layers = this._layers = {};
this._layerConfig = {};

if (!singleCanvas) {//没有画布,就使用div
this._width = this._getSize(0);
this._height = this._getSize(1);

var domRoot = this._domRoot = createRoot(
this._width, this._height
);
root.appendChild(domRoot);
}
else {//已经有块画布
// Use canvas width and height directly
var width = root.width;
var height = root.height;
this._width = width;
this._height = height;

// Create layer if only one given canvas
// dpr设置为1,是因为canvas已经定了宽和高
var mainLayer = new Layer(root, this, 1);
mainLayer.initContext();
// FIXME Use canvas width and height
// mainLayer.resize(width, height);
layers[0] = mainLayer;
zlevelList.push(0);
}

this.pathToImage = this._createPathToImage();
// Layers for progressive rendering
this._progressiveLayers = [];
this._hoverlayer;
this._hoverElements = [];
};

3.Painter.prototype

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
Painter.prototype = {
constructor: Painter,
isSingleCanvas: function() {},
getViewportRoot: function() {},
refresh: function(paintAll) {},
addHover: function(el, hoverStyle) {},
removeHover: function(el) {},
clearHover: function(el) {},
refreshHover: function() {},
_startProgessive: function() {},
_clearProgressive: function() {},
_paintList: function(list, paintAll) {},
_doPaintList: function(list, paintAll) {},
_doPaintEl: function(el, currentLayer, forcePaint, scope) {},
getLayer: function(zlevel) {},
insertLayer: function(zlevel, layer) {},
eachLayer: function(cb, context) {},
eachBuildinLayer: function(cb, context) {},
eachOtherLayer: function(cb, context) {},
getLayers: function() {},
_updateLayerStatus: function(list) {},
clear: function() {},
_clearLayer: function(layer) {},
configLayer: function(zlevel, config) {},
delLayer: function(zlevel) {},
resize: function(width, height) {},
clearLayer: function(zlevel) {},
dispose: function() {},
getRenderedCanvas: function(opts) {},
getWidth: function() {},
getHeight: function() {},
_getSize: function(whIdx) {},
_pathToImage: function(id, path, width, height, dpr) {},
_createPathToImage: function() {}
}

我们再来回顾下整个渲染的过程:
add(zrender.js)–>addRoot(Storage.js) –> addToMap(Storage.js) –>
dirty[标记为脏的,下一帧渲染] (path.js) –> refresh(Painter.js)–>_paintList[遍历_displayList] (Painter.js)–>
_doPaintEl[渲染单个元素] Painter.js) –>brush(Path.js)–>buildPath (各个类型的shape)

  • refresh刷新,刷新去绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 刷新
* @param {boolean} [paintAll=false] 强制绘制所有displayable
*/
refresh: function(paintAll) {
var list = this.storage.getDisplayList(true); //要绘制的图形
var zlevelList = this._zlevelList;
this._paintList(list, paintAll); //去绘制
// Paint custum layers 绘制layer层
for (var i = 0; i < zlevelList.length; i++) {
var z = zlevelList[i];
var layer = this._layers[z];
if (!layer.isBuildin && layer.refresh) {
layer.refresh();
}
}
this.refreshHover(); //刷新hover层
if (this._progressiveLayers.length) {
this._startProgessive();
}
return this;
}
  • _paintList
1
2
3
4
5
6
7
8
9
10
_paintList: function(list, paintAll) {
if (paintAll == null) {
paintAll = false;
}
this._updateLayerStatus(list);
this._clearProgressive();
this.eachBuildinLayer(preProcessLayer);
this._doPaintList(list, paintAll); //全部标注为脏的渲染【dirty(false)】
this.eachBuildinLayer(postProcessLayer);
}
  • _doPaintList

注意这里已经遍历了(遍历的是_displayList数组),所以后面的只针对单个元素绘制即可。

1
2
3
4
5
//... ...
for (var i = 0, l = list.length; i < l; i++) {
//... ...
this._doPaintEl(el, currentLayer, paintAll, scope);//绘制每个元素
}

  • _doPaintEl
1
2
//... ...
el.brush(ctx, scope.prevEl || null);//在Path.js中的方法brush

4.分析Painter对象

这一系列的操作是:

  • 创建canvas外层包裹着_domRoot(div)
  • canvas要绘制的东西都存储在storage中的_displayList数组中
  • 遍历_displayList
  • 最后调用buildPath的canvas绘制。

5.Hover图层

如第1部分所见,如果增加了hover层(addHOver方法),那么会增加一层canvas,现在就来看这一层canvas是如何作用的。
addHover(zrender.js)–>

  • addHover(zrender.js)
1
2
3
4
5
6
addHover: function(el, style) {
if (this.painter.addHover) {
this.painter.addHover(el, style);
this.refreshHover();
}
}
  • addHover(Painter.js)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
addHover: function(el, hoverStyle) {
if (el.__hoverMir) {
return;
}
var elMirror = new el.constructor({
style: el.style,
shape: el.shape
});
elMirror.__from = el;
el.__hoverMir = elMirror;
elMirror.setStyle(hoverStyle);
this._hoverElements.push(elMirror);
//存放到this._hoverElements(数组)
}

我们在第三部分已经看到refresh方法中的refreshHover,渲染canvas时候,会渲染两个canvas,一个是主canvas,一个是hover层canvas,第二个canvas就是使用refreshHover方法。

  • refreshHover(Painter.js)
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
refreshHover: function() {
var hoverElements = this._hoverElements;
var len = hoverElements.length;
var hoverLayer = this._hoverlayer;
hoverLayer && hoverLayer.clear();
if (!len) {
return;
}
timsort(hoverElements, this.storage.displayableSortFunc);
if (!hoverLayer) {//不存在则会新创建一层canvas
hoverLayer = this._hoverlayer = this.getLayer(1e5);
}

var scope = {};
hoverLayer.ctx.save();
for (var i = 0; i < len;) {
var el = hoverElements[i];
var originalEl = el.__from;
if (!(originalEl && originalEl.__zr)) {
hoverElements.splice(i, 1);
originalEl.__hoverMir = null;
len--;
continue;
}
i++;
if (!originalEl.invisible) {
el.transform = originalEl.transform;
el.invTransform = originalEl.invTransform;
el.__clipPaths = originalEl.__clipPaths;
// el.
this._doPaintEl(el, hoverLayer, true, scope);//同第3部分
}
}
hoverLayer.ctx.restore();
}

参考阅读:

  • canvas-mdn教程
  • canvas基本的动画-mdn

ECharts 3.0底层zrender 3.x源码分析3-Handler(C层)

发表于 2017-01-11 | 更新于 2018-11-30 | 分类于 前端技术

这一篇,介绍下Handler处理机制。

Handler负责事件处理,包括’click’, ‘dblclick’, ‘mousewheel’, ‘mouseout’, ‘mouseup’, ‘mousedown’, ‘mousemove’, ‘contextmenu’等。我们知道canvas API没有提供监听每个元素的机制,这就需要一些处理。处理的思路是:监听事件的作用坐标(如点击时候的坐标),判断在哪个绘制元素的范围中,如果在某个元素中,这个元素就监听该事件。

一些demo和没有在博客中介绍的源码请进我的github仓库。

https://github.com/zrysmt/echarts3/tree/master/zrender

1.Handle.js整体

同样Handle.js文件的结构是一个构造函数,一个prototype扩展原型,一些混入模式。

我们首先看在入口(zrender.js)中的调用

1
2
3
var handerProxy = !env.node ? new HandlerProxy(painter.getViewportRoot()) : null;//env.node默认为false
//HandlerProxy 是移动端的一些处理事件
this.handler = new Handler(storage, painter, handerProxy, painter.root);

构造函数:

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
var Handler = function(storage, painter, proxy, painterRoot) {
Eventful.call(this);
this.storage = storage;
this.painter = painter;
this.painterRoot = painterRoot;
proxy = proxy || new EmptyProxy();
/**
* Proxy of event. can be Dom, WebGLSurface, etc.
*/
this.proxy = proxy;
// Attach handler
proxy.handler = this;
this._hovered;
/**
* @private
* @type {Date}
*/
this._lastTouchMoment;
this._lastX;//坐标位置x
this._lastY;//坐标位置y

Draggable.call(this);
util.each(handlerNames, function (name) {
proxy.on && proxy.on(name, this[name], this);
}, this);
};

构造函数中保留的有坐标信息。

prototype中的一个重要的方法dispatchToElement,针对目标图形元素触发事件。

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
/**
* 事件分发代理
*
* @private
* @param {Object} targetEl 目标图形元素
* @param {string} eventName 事件名称
* @param {Object} event 事件对象
*/
dispatchToElement: function(targetEl, eventName, event) {
var eventHandler = 'on' + eventName;
var eventPacket = makeEventPacket(eventName, targetEl, event);
var el = targetEl;
while (el) {
el[eventHandler] && (eventPacket.cancelBubble = el[eventHandler].call(el, eventPacket));
el.trigger(eventName, eventPacket);//触发
el = el.parent;
if (eventPacket.cancelBubble) {
break;
}
}
if (!eventPacket.cancelBubble) {
// 冒泡到顶级 zrender 对象
this.trigger(eventName, eventPacket);
// 分发事件到用户自定义层
// 用户有可能在全局 click 事件中 dispose,所以需要判断下 painter 是否存在
this.painter && this.painter.eachOtherLayer(function(layer) {
if (typeof(layer[eventHandler]) == 'function') {
layer[eventHandler].call(layer, eventPacket);
}
if (layer.trigger) {
layer.trigger(eventName, eventPacket);//触发
}
});
}
}

混入Eventful(发布订阅模式事件)、Draggable(拖动事件)

1
2
util.mixin(Handler, Eventful);
util.mixin(Handler, Draggable);

2.canvas上元素的监听事件

对于一些事件的处理(Handler.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
util.each(['click', 'mousedown', 'mouseup', 'mousewheel', 'dblclick', 'contextmenu'], function (name) {
Handler.prototype[name] = function (event) {
// Find hover again to avoid click event is dispatched manually. Or click is triggered without mouseover
var hovered = this.findHover(event.zrX, event.zrY, null);
if (name === 'mousedown') {
this._downel = hovered;
// In case click triggered before mouseup
this._upel = hovered;
}
else if (name === 'mosueup') {
this._upel = hovered;
}
else if (name === 'click') {
if (this._downel !== this._upel) {
return;
}
}

console.info("hovered:",hovered);
console.info(this);
this.dispatchToElement(hovered, name, event);
};
});

我们在其中打印了this,通过demo/demo1/demo3-chartHasHover.html的例子我们可以发现,点击的时候都会打印this,而且打印3次。

通过打印的hovered,我们可以看出来hovered就是我们点击的对象。

findHover调用的是isHover函数,在isHover函数中通过displayable(Displayable.js)的contain或者rectContain判断点在哪个元素中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function isHover(displayable, x, y) {
if (displayable[displayable.rectHover ? 'rectContain' : 'contain'](x, y)) {
var el = displayable;
while (el) {
// If ancestor is silent or clipped by ancestor
if (el.silent || (el.clipPath && !el.clipPath.contain(x, y))) {
return false;
}
el = el.parent;
}
return true;
}
return false;
}

Displayable.js的contain或者rectContain方法都是调用rectContain方法,判断x,y是否在图形的包围盒上。

1
2
3
4
5
rectContain: function(x, y) {
var coord = this.transformCoordToLocal(x, y);
var rect = this.getBoundingRect();//@module zrender/core/BoundingRect
return rect.contain(coord[0], coord[1]);
}

zrender/core/BoundingRect的contain方法

1
2
3
4
5
contain: function(x, y) {
var rect = this;
return x >= rect.x && x <= (rect.x + rect.width) &&
y >= rect.y && y <= (rect.y + rect.height);
}

我们再来看看,在painter.js中,其实已经为每个元素生成了它的包围盒上。

1
2
3
4
5
6
7
8
9
10
11
12
var tmpRect = new BoundingRect(0, 0, 0, 0);
var viewRect = new BoundingRect(0, 0, 0, 0);

function isDisplayableCulled(el, width, height) {
tmpRect.copy(el.getBoundingRect());
if (el.transform) {
tmpRect.applyTransform(el.transform);
}
viewRect.width = width;
viewRect.height = height;
return !tmpRect.intersect(viewRect);
}

在绘制每个元素的时候,在_doPaintEl方法中调用了isDisplayableCulled。

参考阅读:

  • canvas-mdn教程
  • canvas基本的动画-mdn

ECharts 3.0底层zrender 3.x源码分析1-总体架构

发表于 2017-01-11 | 更新于 2018-11-30 | 分类于 前端技术

zrender是一个轻量级的Canvas类库,作为百度Echarts 3.0的底层基础。截至目前查看的zrender源码和文档,包括官网文档都还停留在2.x时代,我打算用一个系列介绍下zrender 3.x的使用和源码,一些demo和没有在博客中介绍的源码请进我的github仓库。

https://github.com/zrysmt/echarts3/tree/master/zrender

基于版本 3.2.2。

1.总体架构

官网上的一张图和解释。

MVC结构分别在Stroage.js,Painter.js,Handler.js文件下,我们稍后会详细解释,现在我们大概来看下它们分别的作用。

  • Stroage(M) : shape数据CURD管理
  • Painter(V) : canvase元素生命周期管理,视图渲染,绘画,更新控制
  • Handler(C) : 事件交互处理,实现完整dom事件模拟封装
  • shape : 图形实体,分而治之的图形策略,可定义扩展
  • tool : 绘画扩展相关实用方法,工具及脚手架
  • animation : 动画扩展,提供promise式的动画接口和常用缓动函数

源码结构

目录的介绍

  • animation 动画有关;
  • contain 包含判断;
  • container Group.js 元素组的概念;
  • core 核心代码,包含一些工具(util.js)、事件(event.js)、唯一ID(guid.js)、矩阵运算有关(matrix.js)等;
  • dom HandleProxy.js dom事件有关;
  • graphic 图形有关,shape文件夹下就是各个图形的js文件;
  • mixin 混入模式要混入的函数;
  • tool 工具函数,包括颜色工具(color.js),path工具(path.js)和转换工具(transformPath.js);
  • vml IE中的画笔,[vml解释进入](http://www.g168.net/txt/vml/]
  • 全局的文件
    • config.js 配置文件
    • Element.js 元素文件作为zrender最基本的元素
    • Handle.js C层,控制层
    • Layer.js 图层管理
    • Painter.js V层,视图层
    • Storage.js M层,数据管理层
    • zrender.js 入口

2.入口(zrender.js)

2.1 初始化

类似于jquery的无new化处理,init调用即可
调用:

1
var zr = zrender.init(document.getElementById('main'));

源码:

1
2
3
4
5
6
7
var instances = {};    // ZRender实例map索引
var zrender = {};
zrender.init = function(dom, opts) {
var zr = new ZRender(guid(), dom, opts);
instances[zr.id] = zr;
return zr;
};

2.2 构造函数

我们可以在构造函数中,看到MVC的管理机制。

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
var ZRender = function(id, dom, opts) {
opts = opts || {};
this.dom = dom;
this.id = id;
var self = this;
var storage = new Storage();
var rendererType = opts.renderer;
if (useVML) {//IE中使用VML渲染
if (!painterCtors.vml) {
throw new Error('You need to require \'zrender/vml/vml\' to support IE8');
}
rendererType = 'vml';
} else if (!rendererType || !painterCtors[rendererType]) {
rendererType = 'canvas';
}
var painter = new painterCtors[rendererType](dom, storage, opts);

this.storage = storage;//M
this.painter = painter;//V

var handerProxy = !env.node ? new HandlerProxy(painter.getViewportRoot()) : null;
this.handler = new Handler(storage, painter, handerProxy, painter.root);//C

console.log(this);//这里是我增加的为了调试使用的
/**
* @type {module:zrender/animation/Animation}
* 动画控制
*/
this.animation = new Animation({
stage: {
update: zrUtil.bind(this.flush, this)
}
});
this.animation.start();
this._needsRefresh;

// 修改 storage.delFromMap, 每次删除元素之前删除动画
var oldDelFromMap = storage.delFromMap;
var oldAddToMap = storage.addToMap;
storage.delFromMap = function(elId) {
var el = storage.get(elId);
oldDelFromMap.call(storage, elId);
el && el.removeSelfFromZr(self);
};
storage.addToMap = function(el) {
oldAddToMap.call(storage, el);
el.addSelfToZr(self);
};
};

2.3 ZRender.prototype

具体的方法及其注释可以在我的github中查看,这里只将方法名放在这里。

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
ZRender.prototype = {
constructor: ZRender,
/**
* 获取实例唯一标识
* @return {string}
*/
getId: function () {},
/**
* 添加元素后就会渲染
* @param {module:zrender/Element} el
*/
add: function (el) {
this.storage.addRoot(el);
this._needsRefresh = true;
},
/**
* 删除元素
* @param {module:zrender/Element} el
*/
remove: function (el) { },
configLayer: function (zLevel, config) {},
/** Repaint the canvas immediately*/
refreshImmediately: function () {},
/** Mark and repaint the canvas in the next frame of browser*/
refresh: function() {},
flush: function () {},

/**Add element to hover layer */
addHover: function (el, style) {},
/** Add element from hover layer
* @param {module:zrender/Element} el
*/
removeHover: function (el) {},

/** Clear all hover elements in hover layer*/
clearHover: function () {},
/** Refresh hover in next frame*/
refreshHover: function () {},
/**Refresh hover immediately*/
refreshHoverImmediately: function () { ;
},
resize: function(opts) {},
clearAnimation: function () {},
/** Get container width */
getWidth: function() {},
getHeight: function() {},
/** Converting a path to image */
pathToImage: function(e, width, height) {},
/**
* Set default cursor
* @param {string} [cursorStyle='default'] 例如 crosshair
*/
setCursorStyle: function (cursorStyle) {},
/**发布订阅模式 */
on: function(eventName, eventHandler, context) {},
off: function(eventName, eventHandler) {},
trigger: function (eventName, event) {},
/** Clear all objects and the canvas */
clear: function () {},
/** Dispose self */
dispose: function () {}
};

源码的方法,我们以add举例子,它其实调用的是this.storage.addRoot方法,使用MVC机制处理。
使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var circle1 = new Circle({
shape: {
cx: 100,
cy: 100,
r: 30
},
style: {
fill: 'blue'
},
draggable: true
});
zr.add(circle1);
circle1.on('mouseover', function() {
zr.addHover(this, {
stroke: 'yellow',
lineWidth: 10,
opacity: 1
});
zr.refresh();
});
circle1.on('mouseout', function() {
zr.removeHover(this);
});

注意:这里有addHover方法,所以会渲染两个canvas。如果没有addHover,就只会渲染一个canvas。

3.MVC简单概述

MVC对应三个文件的结构很简单,其实就是一个构造函数,一个prototype原型扩展。

3.1 M–数据管理层(Storage.js)

我们看构造函数,将元素存储在this._elements(对象)、this._roots(数组)和this._displayList(数组)中,然后负责在其中进行增(addRoot,addToMap)删(delRoot,delFromMap)改(updateDisplayList)查(get,getDisplayList)。

1
2
3
4
5
6
7
8
9
10
var Storage = function () {
// 所有常规形状,id索引的map
this._elements = {};
//和this._elements存放的元素一样,只不过是数组
this._roots = [];
//和this.roots一样
this._displayList = [];
//this._displayList的长度
this._displayListLen = 0;
};

3.2 C–控制层(Handle.js)

Handler负责事件处理,包括’click’, ‘dblclick’, ‘mousewheel’, ‘mouseout’, ‘mouseup’, ‘mousedown’, ‘mousemove’, ‘contextmenu’等。我们知道canvas API没有提供监听每个元素的机制,这就需要一些处理。处理的思路是:监听事件的作用坐标(如点击时候的坐标),判断在哪个绘制元素的范围中,如果在某个元素中,这个元素就监听该事件。具体的思路可以查看参考阅读给的链接文章。

1
2
3
4
5
Handler.prototype = {
mousemove:function (event){}//... ...
}
util.mixin(Handler, Eventful);//混入,下面我们会解释到
util.mixin(Handler, Draggable);

3.3 V–视图层(Painter.js)

Painter负责真正的绘图操作,这里是比较繁重的部分

  • 1.负责canvas及其周边DOM元素的创建与处理
  • 2.负责调用各个Shape(预定义好的)进行绘制
  • 3.提供基本的操作方法,渲染(render)、刷新(refresh)、尺寸变化(resize)、擦除(clear)等

Painter是调用canvas API实现的绘制,包括颜色,渐变色,变换,矩阵变化,绘制图片、文本等。IE8使用excanvas兼容。

4.设计模式总结

设计模式的总结,我在一篇博客中有写,要想看这方面的知识,可以在这里看。

4.1 AMD模式

AMD即是“异步模块定义”的意思,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。源码的结构是这样的

  • 定义
1
2
3
define(function(require) {
return ZRender;
}
  • 使用
1
require([module], callback);

我们的Demo使用的是百度封装好的AMD模式esl.js(或者使用requirejs也可以),引入方式和使用示例如下:

1
<script src="../libs/esl.js"></script>
1
2
3
require(['zrender', 'zrender/graphic/shape/Circle', 'zrender/graphic/shape/Polygon'],
function(zrender, Circle, Polygon) { //... ...
});

4.2 继承

在core->util.js,主要的思想就是将子类的prototype指向父类的prototype;子类的构造函数指向自己。

1
2
3
4
5
6
7
8
9
10
11
function inherits(clazz, baseClazz) {
var clazzPrototype = clazz.prototype;
function F() {}
F.prototype = baseClazz.prototype;
clazz.prototype = new F();
for (var prop in clazzPrototype) {//属性也继承了
clazz.prototype[prop] = clazzPrototype[prop];
}
clazz.prototype.constructor = clazz;
clazz.superClass = baseClazz;//superClass是个自己定义的属性
}

另外不要忘了,在构造函数中应该重写父类的属性。例如:Displayable的父类是Element:

1
2
3
function Displayable(opts) {
Element.call(this, opts);
}

实现继承:

1
zrUtil.inherits(Displayable, Element);

4.3 混入模式

简而言之,混入就是将一个对象的方法复制给另外一个对象。实现在util.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
function mixin(target, source, overlay) {
target = 'prototype' in target ? target.prototype : target;
source = 'prototype' in source ? source.prototype : source;
defaults(target, source, overlay);
}
function defaults(target, source, overlay) {
for (var key in source) {
if (source.hasOwnProperty(key) && (overlay ? source[key] != null : target[key] == null)) {
target[key] = source[key];
}
}
return target;
}

调用

1
zrUtil.mixin(Displayable, RectText);

4.4 jquery的extend模式

实现很简单,类似混入模式,将source对象的方法复制给target对象。

1
2
3
4
5
6
7
8
function extend(target, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}

4.5 发布订阅模式

逻辑在mixin文件夹中的Eventful.js,为Handle(handle.js)混入方法

1
util.mixin(Handler, Eventful);

包括一下几种方法

  • one一次绑定事件
  • on 绑定事件
  • isSilent是否绑定了事件
  • off解绑事件
  • trigger事件分发,触发事件
  • triggerWithContext带有context的事件分发

5.逻辑关系

  • 步进关系

说明:–>为扩展或混入,==>为继承自父类,()内部为所在位置, [ ]为扩展或者混入的方式。

Element[Animatable Transformable Eventful] (Element.js) ==>
Displayable[ReactText] (Displayable.js) ==>
Path[Sub] (Path.js) ==>
Sub(Path.js) –>
各类型的shape

底层对象是封装过的Element。

  • 绘制的逻辑

add(zrender.js)–>addRoot(Storage.js) –> addToMap(Storage.js) –>
dirty[标记为脏的,下一帧渲染] (path.js) –> refresh(Painter.js)–>_paintList[遍历_displayList] (Painter.js)–>
_doPaintEl[渲染单个元素] Painter.js) –>brush(Path.js)–>buildPath (各个类型的shape)

参考阅读:

  • zrender官方网站
  • vml解释
  • ZRender源码分析系列
  • ZRender源码分析系列源码注释
  • HTML5 Canvas绘制的图形的事件处理

javascript设计模式【下】

发表于 2017-01-03 | 更新于 2018-11-30 | 分类于 前端技术

参考《javascript设计模式》[美]Addy Osmani一书,下面介绍使用javascript经常会使用的主要设计模式。本博文是使用ES5语法的【下】篇,还有一个【上】篇,ES6语法会单独写个博客。
主要是以下几个设计模式:

  • Constructor Pattern 构造模式
  • Module Pattern 模块化模式
  • Revealing Module Pattern 揭露模块化模式
  • Singleton Pattern 单例模式
  • Observer Pattern 观察者模式
  • Mediator Pattern 中介者模式
  • Prototype Pattern 原型模式
  • Command Pattern 命令行模式
  • Facade Pattern 外观模式
  • Factory Pattern 工厂模式
  • Mixin Pattern 混入模式
  • Decorator Pattern 装饰者模式
  • Flyweight Pattern 享元模式

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

6.Facade Pattern 外观模式

外观模式也是只暴露一个很简单的方法,然后该方法在内部执行,调用内部的其他方法,jquery使用了很多这种模式,如$().css、$.ajax()等

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
var module = (function() {
var _private = {
i: 5,
get: function() {
console.log("current value:" + this.i);
},
set: function(val) {
this.i = val;
},
run: function() {
console.log("running");
},
jump: function() {
console.log("jumping");
}
};
return {
facade: function(args) {
_private.set(args.val);
_private.get();
if (args.run) {
_private.run();
}
}
};
}());
// Outputs: "current value: 10" and "running"
module.facade({ run: true, val: 10 });

7.Factory Pattern 工厂模式

怎么解释呢?工厂模式就是创建一个大型的制作工厂,然后其它的对象从这个工厂中产生。

1
2
3
4
5
6
7
8
9
10
11
12
13
//工厂
function VehicleFactory() {}
// Our default vehicleClass is Car 默认的是Car制造工厂
VehicleFactory.prototype.vehicleClass = Car;

VehicleFactory.prototype.createVehicle = function(options) {
if (options.vehicleType === "car") {
this.vehicleClass = Car;
} else {
this.vehicleClass = Truck;
}
return new this.vehicleClass(options);
};

两个具体的工厂里面的制作空间

1
2
3
4
5
6
7
8
9
10
11
12
function Car(options) {
// some defaults
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}
// A constructor for defining new trucks
function Truck(options) {
this.state = options.state || "used";
this.wheelSize = options.wheelSize || "large";
this.color = options.color || "blue";
}

使用

1
2
3
4
5
6
7
8
var carFactory = new VehicleFactory();
var car = carFactory.createVehicle({
vehicleType: "car",
color: "yellow",
doors: 6
});

console.log(car instanceof Car);// Outputs: true

8.Mixin Pattern 混入模式

简单解释下,混入就是将一个对象的方法复制给另外一个对象。

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
var Car = function(settings) {
this.model = settings.model || "no model provided";
this.color = settings.color || "no colour provided";
};

var Mixin = function() {};
Mixin.prototype = {
driveForward: function() {
console.log("drive forward");
},
driveBackward: function() {
console.log("drive backward");
},
driveSideways: function() {
console.log("drive sideways");
}
};

function augment(receivingClass, givingClass) {
if (arguments[2]) {
for (var i = 2, len = arguments.length; i < len; i++) {
receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]];
}
} else {
for (var methodName in givingClass.prototype) {
if (!Object.hasOwnProperty(receivingClass.prototype, methodName)) {
receivingClass.prototype[methodName] = givingClass.prototype[methodName];
}
}
}
}

// 只混入两个方法,Mixin的方法复制给Car
augment(Car, Mixin, "driveForward", "driveBackward");
// Create a new Car
var myCar = new Car({
model: "Ford Escort",
color: "blue"
});
myCar.driveForward();
myCar.driveBackward();

9.Decorator Pattern 装饰者模式

装饰模式是只针对一个基本的对象,添加一些修饰。如下面的是对MacBook,加内存(Memory函数装饰)增加75美元,雕刻(Engraving函数装饰)增加200美元,买保险(Insurance函数装饰)增加250美元。

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
function MacBook() {
this.cost = function() {
return 997; };
this.screenSize = function() {
return 11.6; };
}
// Decorator 1
function Memory(macbook) {
var v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function Engraving(macbook) {
var v = macbook.cost();
macbook.cost = function() {
return v + 200;
};
}
// Decorator 3
function Insurance(macbook) {
var v = macbook.cost();
macbook.cost = function() {
return v + 250;
};

}
var mb = new MacBook();
Memory(mb);
Engraving(mb);
Insurance(mb);

10.Flyweight Pattern 享元模式

享元模式我感觉就是共享一些数据或者方法,有一个工厂可以管理

  • Flyweight
    享元对象(类似于接口),提供的可以共享的属性/方法;
  • Concrete Flyweight
    具体享元对象,实现接口,具体实现享元对象的方法;
  • Flyweight Factory
    享元工厂对象,创建并管理flyweight对象

实现接口的方法,由于js没有,这里我们就模拟下。

1
2
3
4
5
6
7
8
9
10
11
12
13
//在js模拟存虚拟的继承,类似java中的implements
Function.prototype.implementsFor = function(parentClassOrObject) {
if (parentClassOrObject.constructor === Function) {
this.prototype = new parentClassOrObject();
this.prototype.consturctor = this;
this.prototype.parent = parentClassOrObject.prototype;
}else {
//纯虚拟继承
this.prototype = parentClassOrObject;
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject;
}
}

  • 享元对象
1
2
3
4
5
6
// Flyweight object 享元对象
var CoffeeOrder = {
// Interfaces 接口
serveCoffee: function(context) {},
getFlavor: function() {}
};
  • 具体享元对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Implements CoffeeOrder
function CoffeeFlavor(newFlavor) {
var flavor = newFlavor;
if (typeof this.getFlavor === "function") {
this.getFlavor = function() {
return flavor;
};
}

if (typeof this.serveCoffee === "function") {
this.serveCoffee = function(context) {
console.log("Serving Coffee flavor " + flavor + " to table number " + context.getTable());
};
}

}
// Implement interface for CoffeeOrder 实现接口
CoffeeFlavor.implementsFor(CoffeeOrder);
  • 辅助器
1
2
3
4
5
6
7
function CoffeeOrderContext(tableNumber) {
return {
getTable: function() {
return tableNumber;
}
};
}
  • 享元工厂对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//创建并管理flyweight对象
function CoffeeFlavorFactory() {
var flavors = {},
length = 0;

return {
getCoffeeFlavor: function(flavorName) {
//这是个单例模式
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);//创建flyweight对象
flavors[flavorName] = flavor;
length++;
}
return flavor;
},

getTotalCoffeeFlavorsMade: function() {
return length;
}
};
}
  • 测试
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
testFlyweight()

function testFlyweight() {
// The flavors ordered. 已订购的flavors
var flavors = new CoffeeFlavor(),
// The tables for the orders.
tables = new CoffeeOrderContext(),
// Number of orders made 订单数量
ordersMade = 0,
// The CoffeeFlavorFactory instance
flavorFactory;
//flavorIn 订单物的名称
function takeOrders(flavorIn, table) {
flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
//flavorFactory管理者创建好后(管理者也做了处理)返回给CoffeeFlavor
tables[ordersMade++] = new CoffeeOrderContext(table);
}

flavorFactory = new CoffeeFlavorFactory();

takeOrders("Cappuccino", 2);
takeOrders("Cappuccino", 2);
takeOrders("Frappe", 1);
takeOrders("Frappe", 1);
takeOrders("Xpresso", 1);
takeOrders("Frappe", 897);
takeOrders("Cappuccino", 97);
takeOrders("Cappuccino", 97);
takeOrders("Frappe", 3);
takeOrders("Xpresso", 3);
takeOrders("Cappuccino", 3);
takeOrders("Xpresso", 96);
takeOrders("Frappe", 552);
takeOrders("Cappuccino", 121);
takeOrders("Xpresso", 121);

for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}
console.log(" ");
console.log("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade());
}

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

javascript设计模式【上】

发表于 2017-01-03 | 更新于 2018-11-30 | 分类于 前端技术

参考《javascript设计模式》[美]Addy Osmani一书,下面介绍使用javascript经常会使用的主要设计模式。本博文是使用ES5语法的【上】篇,还有一个【下】篇,ES6语法会单独写个博客。
主要是以下几个设计模式:

  • Constructor Pattern 构造模式
  • Module Pattern 模块化模式
  • Revealing Module Pattern 揭露模块化模式
  • Singleton Pattern 单例模式
  • Observer Pattern 观察者模式
  • Mediator Pattern 中介者模式
  • Prototype Pattern 原型模式
  • Command Pattern 命令行模式
  • Facade Pattern 外观模式
  • Factory Pattern 工厂模式
  • Mixin Pattern 混入模式
  • Decorator Pattern 装饰者模式
  • Flyweight Pattern 享元模式

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

1.Module Pattern 模块化模式

模块化很好理解,目前很多提供模块化的库如require.js(AMD),sea.js(CMD),现在我们就看看怎样自己编写的代码能够支持模块化。

1.1 对象字面量表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
var myModule = {
myProperty: "someValue",
myConfig: {
useCaching: true,
language: "en"
},
// a very basic method
myMethod: function() {
console.log("Where in the world is Paul Irish today?");
},
};
// Outputs: Where in the world is Paul Irish today?
myModule.myMethod();

1.2 Module(模块)模式

  • 私有–IIFE模拟
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var testModule = (function() {
var counter = 0;
return {
incrementCounter: function() {
return counter++;
},
resetCounter: function() {
console.log("counter value prior to reset: " + counter);
counter = 0;
}
};
})();
// Usage:

testModule.incrementCounter();
testModule.resetCounter(); //1
  • Module(模块)模式变化–混入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myModule = (function(jQ, _) {
function privateMethod1() {
jQ(".container").html("test");
}
function privateMethod2() {
console.log(_.min([10, 5, 100, 2, 1000]));
}
return {
publicMethod: function() {
privateMethod1();
}
};
}(jQuery, _));

myModule.publicMethod();
  • Module(模块)模式变化–引出
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var myModule = (function() {
    // Module object
    var module = {},
    privateVariable = "Hello World";
    function privateMethod() {
    // ...
    }
    module.publicProperty = "Foobar";
    module.publicMethod = function() {
    console.log(privateVariable);
    };

    return module;
    }());

2.Revealing Module Pattern 揭露模块化模式

其实简单说就是将要暴露的接口返回(return)出去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myRevealingModule = function() {
var privateVar = "Ben Cherry",
publicVar = "Hey there!";

function privateFunction() {
console.log("Name:" + privateVar);
}

function publicSetName(strName) {
privateVar = strName;
}

function publicGetName() {
privateFunction();
}

return {
setName: publicSetName,
greeting: publicVar,
getName: publicGetName
};
}();
myRevealingModule.setName("Paul Kinlan");

3.Singleton Pattern 单例模式

确保实例化或者说是创建对象的时候只实例化/创建一次。

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
var mySingleton = (function() {
// Instance stores a reference to the Singleton
var instance;
function init() {
// Private methods and variables
function privateMethod() {
console.log("I am private");
}
var privateVariable = "Im also private";
var privateRandomNumber = Math.random();
return {
// Public methods and variables
publicMethod: function() {
console.log("The public can see me!");
},
publicProperty: "I am also public",
getRandomNumber: function() {
return privateRandomNumber;
}
};

};
return {
getInstance: function() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();

// Usage:
var singleA = mySingleton.getInstance();
var singleB = mySingleton.getInstance();
console.log( singleA.getRandomNumber() === singleB.getRandomNumber() ); // true
  • 静态方法
    静态方法的解释不太容易说清楚,但可以从它的特点和用处来说明:
    1)静态方法不会被继承
    2)静态方法不用实例化(不用new)能够用直接调用([类名/对象名].[静态方法名])
    ES6在方法前加上static关键字即可,使用ES6实现的见另外一篇博客,或者直接在github中查看我的源代码。使用ES5语法实现见下面:
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
var SingletonTester = (function() {
function Singleton(options) {
options = options || {};
// set some properties for our singleton
this.name = "SingletonTester";
this.pointX = options.pointX || 6;
this.pointY = options.pointY || 10;
}
var instance;
var _static = {
name: "SingletonTester",
getInstance: function(options) {
if (instance === undefined) {
instance = new Singleton(options);
}
return instance;
}
};
return _static;
})();

var singletonTest = SingletonTester.getInstance({
pointX: 5
});

console.log(singletonTest.pointX);// Outputs: 5

3.Observer Pattern 观察者模式

观察者一共有四个组件:

  • Subject: maintains a list of observers, facilitates adding or removing observers
    目标对象(类似接口,不具体实现,只有方法名)
  • Observer: provides a update interface for objects that need to be notified of a Subject’s changes of state
    观察者对象 主要是update方法(类似接口,不具体实现,只有方法名)
  • ConcreteSubject: broadcasts notifications to observers on changes of state, stores the state of ConcreteObservers
    具体目标对象,继承(实现)目标对象(实现接口)
  • ConcreteObserver: stores a reference to the ConcreteSubject, implements an update interface for the Observer to ensure state is consistent with the Subject’s
    具体观察者,继承(实现)观察者(实现接口)

具体观察者模式的代码请移步到我的github中,这里就不单独列出来了。
其实我们现在用的最多的是它的变体-发布-订阅模式
简单解释下该模式,比如我们订阅了某些微信公众号,然后就等着别人发布信息,我们就能立刻接受到信息了。

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
var pubsub = {};
(function(q) {
var topics = {},//存放所有订阅者
subUid = -1;
//发布
q.publish = function(topic, args) {

if (!topics[topic]) {
return false;
}

var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;

while (len--) {
subscribers[len].func(topic, args);
}

return this;
};
//订阅
q.subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}

var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
//取消订阅
q.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return this;
};
}(pubsub));
//测试
var messageLogger = function(topics, data) {
console.log("Logging: " + topics + ": " + data);
};
var subscription = pubsub.subscribe("inbox/newMessage", messageLogger);
pubsub.publish("inbox/newMessage", "hello world!");
// or
pubsub.publish("inbox/newMessage", ["test", "a", "b", "c"]);
// or
pubsub.publish("inbox/newMessage", {
sender: "hello@google.com",
body: "Hey again!"
});

4.Mediator Pattern 中介者模式

该模式和发布订阅模式非常像,这里就不再重复了。

5.Command Pattern 命令行模式

命令行模式就是类似控制台输入命令的方式。说白点就是我们只使用一个方法,第一个参数是我们实际调用的方法,后面的参数是作为该调用方法的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() {
var CarManager = {
requestInfo: function(model, id) {
return "The information for " + model + " with ID " + id + " is foobar";
},
buyVehicle: function(model, id) {
return "You have successfully purchased Item " + id + ", a " + model;
},
arrangeViewing: function(model, id) {
return "You have successfully booked a viewing of " + model + " ( " + id + ") ";
}
};
CarManager.execute = function(name) {
return CarManager[name] && CarManager[name].apply(CarManager, [].slice.call(arguments, 1));
};

console.log(CarManager.execute("arrangeViewing", "Ferrari", "14523"));
console.log(CarManager.execute("requestInfo", "Ford Mondeo", "54323"));
console.log(CarManager.execute("buyVehicle", "Ford Escort", "34232"));
})();

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

javascript设计模式 使用ES6语法

发表于 2017-01-03 | 更新于 2018-11-30 | 分类于 前端技术

参考《javascript设计模式》[美]Addy Osmani一书,下面介绍使用javascript经常会使用的主要设计模式。本博文为ES6语法的博客,还有使用ES5语法的【上】【下】两篇。
主要是以下几个设计模式:

  • Constructor Pattern 构造模式
  • Module Pattern 模块化模式
  • Revealing Module Pattern 揭露模块化模式
  • Singleton Pattern 单例模式
  • Observer Pattern 观察者模式
  • Mediator Pattern 中介者模式
  • Prototype Pattern 原型模式
  • Command Pattern 命令行模式
  • Facade Pattern 外观模式
  • Factory Pattern 工厂模式
  • Mixin Pattern 混入模式
  • Decorator Pattern 装饰者模式
  • Flyweight Pattern 享元模式

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

1.Module Pattern 模块化模式

模块化很好理解,目前很多提供模块化的库如require.js(AMD),sea.js(CMD),现在我们就看看怎样自己编写的代码能够支持模块化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let privateName = Symbol('privateName');//利用Symbol做成私有的变量
//直接用class类
class MyModule {
set container(value) {
this.value = value;
}
get container() {
return this.value;
}
init() {
this.value = 'ES6 module';
}
[privateName](){
console.log("hi");
}
}
export default Module;

2.Singleton Pattern 单例模式

确保实例化或者说是创建对象的时候只实例化/创建一次。

  • 一般的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
let instance = null;
class mySingleton {
constructor() {
if (!instance) instance = this;
return instance;
}
publicMethod() {
console.log("The public can see me!");
}
}

let singleton1 = new mySingleton();
  • 静态方法和单例模式

静态方法的解释不太容易说清楚,但可以从它的特点和用处来说明:
1)静态方法不会被继承
2)静态方法不用实例化(不用new)能够用直接调用([类名/对象名].[静态方法名])

1
2
3
4
5
6
7
8
9
10
11
12
class mySingleton {
static getInstance() {
if (!mySingleton.instance) {
mySingleton.instance = new mySingleton();
}
return mySingleton.instance;
}
publicMethod() {
console.log("The public can see me!");
}
}
var cache = mySingleton.getInstance();

3.Observer Pattern 观察者模式

观察者一共有四个组件:

  • Subject: maintains a list of observers, facilitates adding or removing observers
    目标对象(类似接口,不具体实现,只有方法名)
  • Observer: provides a update interface for objects that need to be notified of a Subject’s changes of state
    观察者对象 主要是update方法(类似接口,不具体实现,只有方法名)
  • ConcreteSubject: broadcasts notifications to observers on changes of state, stores the state of ConcreteObservers
    具体目标对象,继承(实现)目标对象(实现接口)
  • ConcreteObserver: stores a reference to the ConcreteSubject, implements an update interface for the Observer to ensure state is consistent with the Subject’s
    具体观察者,继承(实现)观察者(实现接口)
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
/*Subject 目标*/
class Subject {
addObserver() {
throw new Error("This method must be overwritten!");
}
removeObserver() {
throw new Error("This method must be overwritten!");
}
notify() {
throw new Error("This method must be overwritten!");
}
}

class Observer {
update() {
throw new Error("This method must be overwritten!");
}
}
//=============================================================
//具体的对象
class ControlCheckbox extends Subject {
constructor() {
super();
this.observers = [];
}
addObserver(observer){
this.observers.push(observer);
}
notify(context) {
let observerCount = this.observers.length;
for (let i = 0; i < observerCount; i++) {
this.observers[i].update(context);
}
}
}


//具体的观察者
class AddedCheckboxs extends Observer{
constructor(subject){
super();
console.log(subject);
this.subject = subject;
// this.subject.addObserver(this);
}
update(context){
this.checked = context;
}
}

//main test
let addBtn = document.getElementById("addNewObserver"),
container = document.getElementById("observersContainer"),
controlCheckboxDom = document.getElementById("mainCheckbox");
let controlCheckbox = new ControlCheckbox();
controlCheckboxDom.onclick = function(){
controlCheckbox.notify(controlCheckboxDom.checked);//通知了变化
}

addBtn.onclick = function(){
var check = document.createElement("input");
check.type = "checkbox";
//新增的每一个都应该实现观察者
console.info(controlCheckbox.observers);//查看是否添加上
check.update = AddedCheckboxs.prototype.update;
controlCheckbox.addObserver(check);//添加到观察者列表上去
container.appendChild(check);
}

其实我们现在用的最多的是它的变体-发布-订阅模式
简单解释下该模式,比如我们订阅了某些微信公众号,然后就等着别人发布信息,我们就能立刻接受到信息了

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
//发布-订阅模式
class Pubsub {
constructor(){
this.subUid = 0; //订阅的id值
this.topics = {};//存放所有订阅者
}
publish(topic, args) {
if (!this.topics[topic]) {
return false;
}

let subscribers = this.topics[topic],
len = subscribers ? subscribers.length : 0;

while (len--) {
subscribers[len].func(topic, args);
}

return this;
}

subscribe(topic, func) {
if (!this.topics[topic]) {
this.topics[topic] = [];
}

let token = (++this.subUid).toString();
this.topics[topic].push({
token: token,
func: func
});
return token;
}
unsubscribe(token) {
for (let m in this.topics) {
if (this.topics[m]) {
for (let i = 0, j = this.topics[m].length; i < j; i++) {
if (this.topics[m][i].token === token) {
this.topics[m].splice(i, 1);
return token;
}
}
}
}
return this;
}
}

//usage
let messageLogger = function(topics, data) {
console.log("Logging: " + topics + ": " + data);
};
let pubsub = new Pubsub();
let subscription = pubsub.subscribe("inbox/newMessage", messageLogger);

pubsub.publish("inbox/newMessage", "hello world!");

4.Mediator Pattern 中介者模式

该模式和发布订阅模式非常像,这里就不再重复了

5.Command Pattern 命令行模式

命令行模式就是类似控制台输入命令的方式。说白点就是我们只使用一个方法,第一个参数是我们实际调用的方法,后面的参数是作为该调用方法的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CarManager {
requestInfo(model, id) {
return "The information for " + model + " with ID " + id + " is foobar";
}
buyVehicle(model, id) {
return "You have successfully purchased Item " + id + ", a " + model;
}
arrangeViewing(model, id) {
return "You have successfully booked a viewing of " + model + " ( " + id + ") ";
}
execute(name) {
let carManager = new CarManager();
return carManager[name] && carManager[name].apply(carManager, [].slice.call(arguments, 1));
}
}
let carManager = new CarManager();
console.log(carManager.execute("arrangeViewing", "Ferrari", "14523"));
console.log(carManager.execute("requestInfo", "Ford Mondeo", "54323"));
console.log(carManager.execute("requestInfo", "Ford Escort", "34232"));

6.Facade Pattern 外观模式

外观模式也是只暴露一个很简单的方法,然后该方法在内部执行,调用内部的其他方法,jquery使用了很多这种模式,如$().css、$.ajax()等。

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
//装饰者模式
class Facade {
_get() {
console.log("current value:" + this.i);
}
_set(val) {
this.i = val;
}
_run() {
console.log("running");
}
_jump() {
console.log("jumping");
}

facade(args) {
this._set(args.val);
this._get();
if (args._run) {
this._run();
}
}
}
let fa = new Facade();
fa.facade({ run: true, val: 10 });

7.Factory Pattern 工厂模式

怎么解释呢?工厂模式就是创建一个大型的制作工厂,然后其它的对象从这个工厂中产生。

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
class VehicleFactory {
constructor() {
this.vehicleClass = Car;
}
createVehicle(options) {
if (options.vehicleType === "car") {
this.vehicleClass = Car;
} else {
this.vehicleClass = Truck;
}
return new this.vehicleClass(options);
}
}

class Car {
constructor(options) {
// some defaults
options = options || "";
this.doors = options.doors || 4;
this.state = options.state || "brand new";
this.color = options.color || "silver";
}
}
class Truck {
constructor(options) {
this.state = options.state || "used";
this.wheelSize = options.wheelSize || "large";
this.color = options.color || "blue";
}
}
//usage
let carFactory = new VehicleFactory();
let car = carFactory.createVehicle({
vehicleType: "car",
color: "yellow",
doors: 6
});

8.Mixin Pattern 混入模式

简单解释下,混入就是将一个对象的方法复制给另外一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//http://es6.ruanyifeng.com/#docs/class#Mixin模式的实现
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor" && key !== "prototype" && key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
//使用-继承即可
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}

9.Decorator Pattern 装饰者模式

装饰模式是只针对一个基本的对象,添加一些修饰。如下面的是对MacBook,加内存(Memory函数装饰)增加75美元,雕刻(Engraving函数装饰)增加200美元,买保险(Insurance函数装饰)增加250美元。

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
class MacBook {
cost() {
return 997;
}
screenSize() {
return 11.6;
}
}

function Memory(macbook) {
let v = macbook.cost();
macbook.cost = function() {
return v + 75;
};
}
// Decorator 2
function Engraving(macbook) {

let v = macbook.cost();
macbook.cost = function() {
return v + 200;
};
}
// Decorator 3
function Insurance(macbook) {
let v = macbook.cost();
macbook.cost = function() {
return v + 250;
};
}
let mb = new MacBook();
Memory(mb);
Engraving(mb);
Insurance(mb);
console.log(mb.cost());// Outputs: 1522
console.log(mb.screenSize());// Outputs: 11.6

10.Flyweight Pattern 享元模式

享元模式我感觉就是共享一些数据或者方法,有一个工厂可以管理

  • Flyweight
    享元对象(类似于接口),提供的可以共享的属性/方法;
  • Concrete Flyweight
    具体享元对象,实现接口,具体实现享元对象的方法;
  • Flyweight Factory
    享元工厂对象,创建并管理flyweight对象

实现接口的方法,由于js没有,我们需要模拟。
这部分没有必要完全使用ES6语法,请参考我的上一篇博客。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//在js模拟存虚拟的继承,类似java中的implements
Function.prototype.implementsFor = function(parentClassOrObject) {
if (parentClassOrObject.constructor === Function) {
this.prototype = new parentClassOrObject();
this.prototype.consturctor = this;
this.prototype.parent = parentClassOrObject.prototype;
}else {
//纯虚拟继承
this.prototype = parentClassOrObject;
this.prototype.constructor = this;
this.prototype.parent = parentClassOrObject;
}
}

// Flyweight object 享元对象
var CoffeeOrder = {
// Interfaces 接口
serveCoffee: function(context) {},
getFlavor: function() {}
};
// ConcreteFlyweight object that creates ConcreteFlyweight 具体享元对象
function CoffeeFlavor(newFlavor) {
var flavor = newFlavor;
if (typeof this.getFlavor === "function") {
this.getFlavor = function() {
return flavor;
};
}
if (typeof this.serveCoffee === "function") {
this.serveCoffee = function(context) {
console.log("Serving Coffee flavor " + flavor + " to table number " + context.getTable());
};
}
}
// Implement interface for CoffeeOrder 实现接口
CoffeeFlavor.implementsFor(CoffeeOrder);
// tableNumber 订单数 辅助器
function CoffeeOrderContext(tableNumber) {
return {
getTable: function() {
return tableNumber;
}
};
}
//享元工厂对象
//创建并管理flyweight对象
function CoffeeFlavorFactory() {
var flavors = {},
length = 0;
return {
getCoffeeFlavor: function(flavorName) {
//这是个单例模式
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);//创建flyweight对象
flavors[flavorName] = flavor;
length++;
}
return flavor;
},
getTotalCoffeeFlavorsMade: function() {
return length;
}
};
}

// Sample usage: 测试
testFlyweight()
function testFlyweight() {
// The flavors ordered. 已订购的flavors
var flavors = new CoffeeFlavor(),
// The tables for the orders.
tables = new CoffeeOrderContext(),
// Number of orders made 订单数量
ordersMade = 0,
// The CoffeeFlavorFactory instance
flavorFactory;
//flavorIn 订单物的名称
function takeOrders(flavorIn, table) {
flavors[ordersMade] = flavorFactory.getCoffeeFlavor(flavorIn);
//flavorFactory管理者创建好后(管理者也做了处理)返回给CoffeeFlavor
tables[ordersMade++] = new CoffeeOrderContext(table);
}

flavorFactory = new CoffeeFlavorFactory();

takeOrders("Cappuccino", 2);
takeOrders("Cappuccino", 2);
takeOrders("Frappe", 1);
takeOrders("Frappe", 1);
takeOrders("Xpresso", 1);
for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}
console.log("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade());
}

所有代码挂在我的github上,包含有ES5和ES6语法实现的内容。
https://github.com/zrysmt/javascript-design-pattern

一步一步DIY zepto库,研究zepto源码1

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

我在之前写了《一步一步DIY jQuery库》系列文章,然后发现再往下进行研究jQuery库的时候,由于jQuery库做了很多兼容IE6-8的内容,使其看起来比较繁琐,这也造成了jQuery源码的不宜读性。所幸作为移动端的jQuery库替代品-Zepto,是一个轻量级的针对现代高级浏览器的JavaScript库, 它与jquery有着类似的api。Zepto的设计目的是提供 jQuery 的类似的API,但并不是100%覆盖 jQuery。

接下来我们会用一系列博客一边研究Zepto源码,一边DIY一个Zepto库。

基于Zepto 1.2.0版本。

代码挂在我的github上,第一篇博客对应文件夹v0.1。
https://github.com/zrysmt/DIY-zepto

1.下载源码并且编译

在github中:https://github.com/madrobby/zeptoclone下源码,使用下面的命令在命令行编译:

1
2
npm install
npm run dist

生成源码文件

1
2
`dist/zepto.js`
`dist/zepto.min.js`

2.整体结构

1
2
3
4
var Zepto = (function() {
})();
window.Zepto = Zepto;
window.$ === undefined && (window.$ = Zepto);

Zepto没有提供noConflict命名冲突处理机制,$被占用后,就只能用Zepto。

3.无new化处理结构

使用

1
console.log($('<p></p>'));//生成dom(<p></p>)

1
2
3
$ = function(selector, context) {
return zepto.init(selector, context)
}

实际上是调用zepto.init

1
2
3
4
zepto.init = function(selector, context) {
//... ...
return zepto.Z(dom, selector);
}

调用zepto.Z

1
2
3
4
5
6
7
8
9
10
11
function Z(dom, selector) {
var i, len = dom ? dom.length : 0;
for (i = 0; i < len; i++) {
this[i] = dom[i];
this.length = len;//NodeList对象一定要有length属性
this.selector = selector || '';//选择符
}
}
zepto.Z = function(dom, selector) {
return new Z(dom, selector);//在这里使用new实例化
}

4.传参 形如$('<p></p>')

4.1 定义要使用的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var emptyArray = [],
concat = emptyArray.concat,
filter = emptyArray.filter,
slice = emptyArray.slice,
fragmentRE = /^\s*<(\w+|!)[^>]*>/,
singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
rootNodeRE = /^(?:body|html)$/i,
table = document.createElement('table'),
tableRow = document.createElement('tr'),
containers = {
'tr': document.createElement('tbody'),
'tbody': table,
'thead': table,
'tfoot': table,
'td': tableRow,
'th': tableRow,
'*': document.createElement('div')
},
class2type = {},
toString = class2type.toString,
zepto = {};

4.2 要使用的工具函数

  • 判断类型模块

这些函数都比较好理解

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
function type(obj) {
return obj == null ? String(obj) :
class2type[toString.call(obj)] || "object";
}
function isWindow(obj) {
return obj != null && obj == obj.window
}
function isObject(obj) {
return type(obj) == "object";
}
isArray = Array.isArray ||
function(object){ return object instanceof Array }
function likeArray(obj) {
var length = !!obj && 'length' in obj && obj.length,
type = $.type(obj)

return 'function' != type && !isWindow(obj) && (
'array' == type || length === 0 ||
(typeof length == 'number' && length > 0 && (length - 1) in obj)
)
}
//加上下面这些就可以使用类型判断了
$.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});

  • 在$后面定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$.trim = function(str) {
return str == null ? "" : String.prototype.trim.call(str);
}
$.each = function(elements, callback) {
var i, key;
if (likeArray(elements)) {
for (i = 0; i < elements.length; i++)
if (callback.call(elements[i], i, elements[i]) === false) return elements;
} else {
for (key in elements)
if (callback.call(elements[key], key, elements[key]) === false) return elements;
}

return elements
}
$.type = type;
$.isArray = isArray;

4.4 zepto.init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
zepto.init = function(selector, context) {
var dom;
//未传参,返回空Zepto对象
if (!selector) {
return zepto.Z();
} else if (typeof selector == 'string') {
selector = selector.trim();
//如果是“<>”,基本的html代码时
if (selector[0] == '<' && fragmentRE.test(selector)) {
//调用片段生成dom
dom = zepto.fragment(selector, RegExp.$1, context), selector = null;//@1
}
//TODO:带有上下文和css查询
} //如果selector是一个Zepto对象,返回它自己
else if (zepto.isZ(selector)) {
return selector;
} else {
if (isObject(selector)) {
dom = [selector], selector = null;//@3
}
}
return zepto.Z(dom, selector);
}

4.5 zepto.fragment

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
/**
* [fragment 内部函数 HTML 转换成 DOM]
* @param {[String]} html [html片段]
* @param {[String]} name [容器标签名]
* @param {[Object]} properties [附加的属性对象]
* @return {[*]}
*/
zepto.fragment = function(html, name, properties) {
var dom, nodes, container;
if (singleTagRE.test(html)) {
dom = $(document.createElement(RegExp.$1));//@2
}
if (!dom) {
//修正自闭合标签<input/>转换为<input></input>
if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>");
if (name === undefined) name = fragmentRE.test(html) && RegExp.$1;
//设置容器名,如果不是tr,tbody,thead,tfoot,td,th,则容器名为div
if (!(name in containers)) name = "*";
container = containers[name]; //创建容器
container.innerHTML = '' + html; //生成DOM
//取容器的子节点
dom = $.each(slice.call(container.childNodes), function() {
container.removeChild(this);
});
//TODO 第三个参数properties带有属性
}
return dom;
}

整体流程:

1
$('<p></p>')

zepto.init–> @1 –>zepto.fragment–> @2 –> 函数 $ –> zepto.init
–> @3 –> return zepto.Z(dom, selector) –> 函数 Z 返回结果。

5.$.fn扩展

1
2
3
4
5
$.fn = {
log: function(test) {
return '测试';
}
};

使用时候:

1
2
console.log($.fn.log());//测试
console.log($('div').log());//undefined

$('div')返回的是Z对象(isZ返回true)。
注意,我们在外部只暴露了Zepto,zepto,Z,zepto.Z是内部的变量。
我们缺少一步,将Z.prototype指向$.fn

1
zepto.Z.prototype = Z.prototype = $.fn;

这个时候我们的例子都能正确使用了。

6.链式调用

其实原理很简单,只要return this;即可。
在$.fn中需要

1
2
3
4
$.fn = {
constructor: zepto.Z,
length: 0,//为了链式调用能够return this;
};

constructor和length都是为了指定this的constructor,增加默认length属性。而且我们在设置zepto.Z.prototype = Z.prototype = $.fn;等于重写了zepto.Z和Z的原型链,需要使用constructor: zepto.Z重新使原型链连接上。

7.$.extend扩展

  • 工具
1
2
3
function isPlainObject(obj) {
return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype;
}
  • $.extend
1
2
3
4
5
6
7
8
9
$.extend = function(target) {
var deep, args = slice.call(arguments, 1);
if (typeof target == 'boolean') {
deep = target;
target = args.shift();
}
args.forEach(function(arg) { extend(target, arg, deep); });
return target
}
  • extend函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function extend(target, source, deep) {
for (key in source)
// deep=true深拷贝 source[key]是数组,一层一层剥开
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}; //target[key]不是对象的时候,返回空
// source[key]是数组,target[key]不是数组
if (isArray(source[key]) && !isArray(target[key]))
target[key] = [];
extend(target[key], source[key], deep); //递归
} else if (source[key] !== undefined) { //递归结束,source[key]不是数组
target[key] = source[key];
}
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
var target = {
one: 'patridge',
three: ["apple", "patato"],
five: { w: "10", a: "20" }
},
source2 = {
three: ["apple1", "patato1", "abc"],
four: "orange",
five: { w: "100", h: "200" }
};
// console.log($.extend(target, source2));//差别:five:{w:"100",h:"200"}
console.log($.extend(true,target, source2));//差别:five:{a:"20" h:"200" w:"100"}

8.CSS选择器查询

完成zepto.init中TODO:带有上下文和css查询。毕竟这些才是我们经常使用的。

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
zepto.init = function(selector, context) {
var dom;
//未传参,返回空Zepto对象
if (!selector) {
console.log("未传参数");
return zepto.Z();
} else if (typeof selector == 'string') {
selector = selector.trim();
//如果是“<>”,基本的html代码时
if (selector[0] == '<' && fragmentRE.test(selector)) {
console.log(selector, RegExp.$1);
//调用片段生成dom
dom = zepto.fragment(selector, RegExp.$1, context), selector = null;
//TODO:带有上下文和css查询
/********增加代码*****************************************/
} else if (context !== undefined) {
return $(context).find(selector);
} else {
dom = zepto.qsa(document, selector)
}
/******************************************************/
} //如果selector是一个Zepto对象,返回它自己
else if (zepto.isZ(selector)) {
return selector;
} else {
if (isObject(selector)) {
dom = [selector], selector = null;
}
}
return zepto.Z(dom, selector);
}

  • qsa函数定义

  • 需要的工具和变量

1
2
simpleSelectorRE = /^[\w-]*$/;//全局变量
//匹配包括下划线的任何单词字符或者 -
  • qsa函数
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
/**
* [qsa CSS选择器]
* @param {[ELEMENT_NODE]} element [上下文,常用document]
* @param {[String]} selector [选择器]
* @return {[NodeList ]} [查询结果]
*/
zepto.qsa = function(element, selector) {
var found,
maybeID = selector[0] == '#',
maybeClass = !maybeID && selector[0] == '.',
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
isSimple = simpleSelectorRE.test(nameOnly); //匹配包括下划线的任何单词字符或者 -
return (element.getElementById && isSimple && maybeID) ? //Safari DocumentFragment 没有 getElementById
//根据id号去查,有返回[found],无返回[]
((found = element.getElementById(nameOnly)) ? [found] : []) :
//不是元素(ELEMENT_NODE),DOCUMENT_NODE,DOCUMENT_FRAGMENT_NODE,返回空[]
(element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
//是上述类型,转化为数组
slice.call(
//DocumentFragment 没有getElementsByClassName/TagName
isSimple && !maybeID && element.getElementsByClassName ?
maybeClass ? element.getElementsByClassName(nameOnly) : //通过类名获得
element.getElementsByTagName(selector) : //通过tag标签名获得
element.querySelectorAll(selector) //不支持getElementsByClassName/TagName的
);
};

这篇博文已经把Zepto的基本架构构建出来了,当然这远远不够,甚至zepto.init都没完全实现,下一篇博文将首先完全实现zepto.init

代码挂在我的github上,第一篇博客对应文件夹v0.1。

参考阅读:

  • Zepto.js API中文版
  • https://github.com/madrobby/zepto

一步一步DIY zepto库,研究zepto源码6--deferred

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

接下来我们来DIY另外一个重要的模块defrred延迟对象,这当然与源码有些许的不同,然而这并不重要。

基础包上要进行扩展了,输入命令:

1
MODULES="zepto event ajax deferred callbacks" npm run dist

代码挂在我的github上,对应文件夹v0.6.1。
https://github.com/zrysmt/DIY-zepto

1.示例Demo

示例1:

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
$.ajax({
type: 'GET',
// type: 'POST',
url: '/projects.json',
dataType: 'json',
timeout: 300,
success: function(data) {
console.log(data);
},
error: function(xhr, type) {
alert('Ajax error!');
}
}).done(function() {
console.info("done");
}).fail(function() {
console.info("fail");
}).always(function() {
console.info("always");
}) //then 三个参数 第一个是成功后回掉,第二个是失败,第三个是运行中
.then(function() {
console.info("then1");
}, function() {
console.info("then2");
}, function() {
console.info("then3");
});

成功后结果:

1
//done always then1

失败后结果:

1
//alert('Ajax error!')  fail always then2

示例2:

1
2
3
4
5
6
7
8
9
10
11
var wait = function(dtd) {    
var dtd = $.Deferred(); //在函数内部,新建一个Deferred对象    
var tasks = function() {      
alert("执行完毕!");      
dtd.resolve(); // 改变Deferred对象的执行状态     
};  
setTimeout(tasks, 5000);    
return dtd.promise(); // 返回promise对象
// 返回dtd.promise 因其没有resolve和reject方法,所以在外面不能该调用这两个方法改变状态
  
};

1
2
3
4
5
$.when(wait()).done(function() {
alert("哈哈,成功了!");
})  .fail(function() {
alert("出错啦!");
});

或者:

1
2
3
4
5
$.Deferred(wait).done(function() {
alert("哈哈,成功了!");
})  .fail(function() {
alert("出错啦!");
});

2.整体结构

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
var DeferredMod = function($) {
var slice = Array.prototype.slice;

function Deferred(func) {
var tuples ,
state = "pending", //Promoise的初始状态
promoise = {
state: function() {
//... ...
},
always: function() {
//... ...
},
then: function( /* fnDone [, fnFailed [, fnProgress]] */ ) {
//... ...
},
//返回obj的promise对象
promise: function(obj) {
//... ...
}
},
deferred = {};
//给deferred添加切换状态方法
$.each(tuples, function(i, tuple) {
});
//deferred包装成promise 继承promise对象的方法
//调用promoise的promoise方法
promoise.promise(deferred);
//传递了参数func,执行
if (func) func.call(deferred, deferred);
//返回deferred对象
return deferred;
}
$.when = function(sub){ };
$.Deferred = Deferred;
};

export default DeferredMod;

3.Promise规范

由于deferred是基于Promise规范,我们首先需要理清楚Promises/A+是什么。
它的规范内容大致如下

  • 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
  • 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
  • promise必须实现then方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  • then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象

伪代码实现:

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
//初始化: 等待状态  pending
var Promise = {
status: pending, //状态
promise: function(o) {
return {
done: done,
fail: fail
}
},
//必须申明的then方法
then: function(fulfilledFn, rejectedFn) {
this.done(fulfilledFn);
this.fail(rejectedFn);
//返回promise对象
return this;
},
//当状态切换fulfilled时执行
done: function() { },
//当状态切换rejected时执行
fail: function() { },
//切换为已完成状态
toFulfilled: function() {
this.status = 'fulfilled'
},
//切换为已拒绝状态
toRejected: function() {
this.status = 'rejected'
}
}

//将函数包装成Promise对象,并注册完成、拒绝链方法
//通过then
Promise.promise(fA).then(fa1, efa1).then(fa2, efa2);
//假定fb里还调用了另一个异步FB,
//之前fA的异步回调执行到fb方法
var PA = Promise.promise(fA).then(fa, efa).then(fb, efb);
//再挂上fB的异步回调
PA.then(fB).then(fb1, efb1).then(fb2, efb2);

4.Deferred函数

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
function Deferred(func) {
/*********************变量***************************************/
//元组:描述状态、状态切换方法名、对应状态执行方法名、回调列表的关系
//tuple引自C++/python,和list的区别是,它不可改变 ,用来存储常量集
var tuples = [
// action, add listener, listener list, final state
["resolve", "done", $.Callbacks({ once: 1, memory: 1 }), "resolved"],
["reject", "fail", $.Callbacks({ once: 1, memory: 1 }), "rejected"],
["notify", "progress", $.Callbacks({ memory: 1 })]
],
state = "pending", //Promoise的初始状态
//promise对象,promise和deferred的区别是:
/*promise只包含执行阶段的方法always(),then(),done(),fail(),progress()及辅助方法state()、promise()等。
deferred则在继承promise的基础上,增加切换状态的方法,resolve()/resolveWith(),reject()/rejectWith(),notify()/notifyWith()*/
//所以称promise是deferred的只读副本
promise = {
// 返回状态
state: function() {
return state;
},
//成功/失败均回调调用
always: function() {
deferred.done(arguments).fail(arguments);
return this;
},
then: function( /* fnDone [, fnFailed [, fnProgress]] */ ) {
var fns = arguments;
//注意,这无论如何都会返回一个新的Deferred只读副本,
//返回Deferred(func)函数,传递一个函数作为参数func,
//`if (func) func.call(deferred, deferred);`执行func,这个时候defer就是deferred
return Deferred(function(defer) {
$.each(tuples, function(i, tuple) {
//i==0: done i==1: fail i==2 progress
var fn = $.isFunction(fns[i]) && fns[i];
//执行新deferred done/fail/progress
deferred[tuple[1]](function() {
//直接执行新添加的回调 fnDone fnFailed fnProgress
var returned = fn && fn.apply(this, arguments);
if (returned && $.isFunction(returned.promise)) {
//转向fnDone fnFailed fnProgress返回的promise对象
//注意,这里是两个promise对象的数据交流
//新deferrred对象切换为对应的成功/失败/通知状态,传递的参数为 returned.promise() 给予的参数值
returned.promise()
.done(defer.resolve)
.fail(defer.reject)
.progress(defer.notify);
} else {
var context = this === promise ? defer.promise() : this,
values = fn ? [returned] : arguments;
defer[tuple[0] + "With"](context, values); //新deferrred对象切换为对应的成功/失败/通知状态
}
});
});
fns = null;
}).promise(); // 返回deferrred.promise 因其没有resolve和reject方法,所以在外面不能该调用这两个方法改变状态
},
//返回obj的promise对象
promise: function(obj) {
return obj != null ? $.extend(obj, promise) : promise;
}
}
}
  • 这里需要重点注意的是:promise和deferred的关系:

promise只包含执行阶段的方法always(),then(),done(),fail(),progress()及辅助方法state()、promise()等。

deferred则在继承promise的基础上(第5部分:promise.promise(deferred)),增加切换状态的方法,resolve()/resolveWith(),reject()/rejectWith(),notify()/notifyWith()

所以称promise是deferred的只读副本


  • 赋予resolve、reject、notify真正的含义是在then中
1
2
3
4
5
6
7
8
//切换的状态是resolve成功/reject失败
//添加首组方法做预处理,修改state的值,使成功或失败互斥,锁定progress回调列表
if (stateString) {
list.add(function() {
state = stateString;
//i^1 ^异或运算符 0^1=1 1^1=0,成功或失败回调互斥,调用一方,禁用另一方
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}//因为回调函数带有memory,add后立刻执行

  • 提供的deferred.promise()方法的作用是,在原来的Deferred 对象上返回另一个 Deferred 对象,即受限制的 Promise 对象,受限制的 Promise 对象只开放与改变执行状态无关的方法(比如done()方法和fail()方法),屏蔽与改变执行状态有关的方法(比如resolve()方法和reject()方法),从而使得执行状态不能被改变

5.给deferred添加切换状态方法

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
//给deferred添加切换状态方法
$.each(tuples, function(i, tuple) {
var list = tuple[2], //$.Callback
stateString = tuple[3]; //状态 一共2个:resolved rejected

//扩展promise的done、fail、progress为Callback的add方法,使其成为回调列表
//简单写法:
// promise['done'] = $.Callbacks( "once memory" ).add
// promise['fail'] = $.Callbacks( "once memory" ).add
// promise['progress'] = $.Callbacks( "memory" ).add
// 使用的时候 .done(func) func就添加到了回调函数中
promise[tuple[1]] = list.add;

//切换的状态是resolve成功/reject失败
//添加首组方法做预处理,修改state的值,使成功或失败互斥,锁定progress回调列表
if (stateString) {
list.add(function() {
state = stateString;
//i^1 ^异或运算符 0^1=1 1^1=0,成功或失败回调互斥,调用一方,禁用另一方
}, tuples[i ^ 1][2].disable, tuples[2][2].lock);
}//因为回调函数带有memory,add后立刻执行.包括上面的 promise[tuple[1]]
//添加切换状态方法 resolve()/resolveWith(),reject()/rejectWith(),notify()/notifyWith()
deferred[tuple[0]] = function() {
//使用list.fireWith 调用
deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments);
return this;
};
//使用 deferred.resolveWith()就使用fireWith调用了回调函数
deferred[tuple[0] + "With"] = list.fireWith;
});
//deferred包装成promise 继承promise对象的方法
//调用promise的promise方法
promise.promise(deferred);
//传递了参数func,执行
if (func) func.call(deferred, deferred);

//返回deferred对象
return deferred;
}

6.$.when

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
/**
* 主要用于多异步队列处理。
多异步队列都成功,执行成功方法,一个失败,执行失败方法
也可以传非异步队列对象
* @param sub
* @returns {*}
*/
$.when = function(sub) {
var resolveValues = slice.call(arguments), //队列数组 ,未传参数是[]
len = resolveValues.length, //队列个数
i = 0,
remain = len !== 1 || (sub && $.isFunction(sub.promise)) ? len : 0, //子deferred计数
deferred = remain === 1 ? sub : Deferred(), //主def,如果是1个fn,直接以它为主def,否则建立新的Def
progressValues, progressContexts, resolveContexts,
updateFn = function(i, ctx, val) {
return function(value) {
ctx[i] = this;
val[i] = arguments.length > 1 ? slice.call(arguments) : value; // val 调用成功函数列表的参数
if (val === progressValues) {
deferred.notifyWith(ctx, val); // 如果是通知,调用主函数的通知,通知可以调用多次
} else if (!(--remain)) { //如果是成功,则需等成功计数为0,即所有子def都成功执行了,remain变为0
deferred.resolveWith(ctx, val); //调用主函数的成功
}
};
};

if (len > 1) {
progressValues = new Array(len);
progressContexts = new Array(len);
resolveContexts = new Array(len);
for (; i < len; ++i) {
if (resolveValues[i] && $.isFunction(resolveValues[i].promise)) {
resolveValues[i].promise()
.done(updateFn(i, resolveContexts, resolveValues)) //每一个成功
.fail(deferred.reject) //直接挂入主def的失败通知函数,当某个子def失败时,
//调用主def的切换失败状态方法,执行主def的失败函数列表
.progress(updateFn(i, progressContexts, progressValues));
} else {
--remain; //非def,直接标记成功,减1
}
}
}
//都为非def,比如无参数,或者所有子队列全为非def,直接通知成功,进入成功函数列表
if (!remain) deferred.resolveWith(resolveContexts, resolveValues);
return deferred.promise();
};

示例可以见第一部分示例2

全部代码挂在我的github上,本博文对应文件夹v0.6.x。
https://github.com/zrysmt/DIY-zepto

参考阅读:

  • Zepto源码分析-deferred模块
  • jQuery的deferred对象详解
  • deferred.resolveWith()

一步一步DIY zepto库,研究zepto源码2

发表于 2016-12-05 | 更新于 2018-11-30 | 分类于 前端技术

我们接着上一篇博客继续完成zepto.init的其余内容。

基于Zepto 1.2.0版本。

代码挂在我的github上,第一篇博客对应文件夹v0.2。
https://github.com/zrysmt/DIY-zepto

整体的流程是:

  • 有传入context,回调自身:$(context).find(selector)
  • selector参数为空,直接调用$.zepto.Z方法获取Z对象:zepto.Z()
  • selector参数为html片段,调用$.zepto.fragment方法获取对应DOM节点再调用$.zeptoZ方法获取Z对象
  • selector参数为css选择器,调用$.zepto.qsa方法获取对应DOM节点再调用$.zepto.Z方法获取Z对象
  • selector参数为DOM节点数组,去掉数组中值为null的项,调用$.zepto.Z方法获取Z对象
  • selector参数为单个DOM节点,dom = [selector],然后调用$.zepto.Z方法获取Z对象
  • selector参数为Z对象,直接返回该Z对象
  • selector参数为函数,执行$(document).ready(selector),在DOM加载完的时候调用该函数

整体代码:

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
zepto.init = function(selector, context) {
var dom;
//未传参,返回空Zepto对象
if (!selector) {
console.log("未传参数");
return zepto.Z();
} else if (typeof selector == 'string') {
selector = selector.trim();
//如果是“<>”,基本的html代码时
if (selector[0] == '<' && fragmentRE.test(selector)) {
console.log(selector, RegExp.$1);
//调用片段生成dom
dom = zepto.fragment(selector, RegExp.$1, context), selector = null;
} else if (context !== undefined) {
return $(context).find(selector);
} else {
dom = zepto.qsa(document, selector)
}
} //如果selector是个函数
else if (isFunction(selector)) {
return $(document).ready(selector);
} //如果selector是一个Zepto对象,返回它自己
else if (zepto.isZ(selector)) {
return selector;
} else {
if (isArray(selector)) {
dom = compact(selector);
} else if (isObject(selector)) {
dom = [selector], selector = null;//单个DOM
} else if (fragmentRE.test(selector)) {
dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null;
} else if (context !== undefined) {
return $(context).find(selector);
} else {
dom = zepto.qsa(document, selector);
}
}
return zepto.Z(dom, selector);
}

1.上下文查找

关键一步是$(context).find(selector);,通过find函数查找(1.5部分)。

1
2
3
4
5
6
7
8
<div id="foo1">
<div>1</div>
<div>2</div>
</div>
<script src="_zepto.js"></script>
<script type="text/javascript">
console.log($('div', '#foo1'));//foo1下的两个div
</script>

1.1 工具及变量

1
2
3
4
5
6
7
8
//清除包含的null undefined
function compact(array) {
return filter.call(array, function(item) {
return item != null;
});
}
function isFunction(value) { return type(value) == "function"; }
$.isFunction = isFunction;

1.2 filter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$.fn = {
//fiter函数其实可以说是包装原生的filter方法
filter: function(selector) {
if (isFunction(selector)) {
//this.not(selector)取到需要排除的集合,
//第二次再取反(这个时候this.not的参数就是一个集合了),得到想要的集合
return this.not(this.not(selector));//见`1.3`部分
} //下面一句的filter是原生的方法
//过滤剩下this中有被selector选择的
return $(filter.call(this, function(element) {
return zepto.matches(element, selector);//见`1.4`部分
}));
}
}
}

1.3 not、forEach方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$.fn = {
forEach: emptyArray.forEach,
not: function(selector) {
var nodes = [];
//当selector为函数时,safari下的typeof NodeList也是function,
//所以这里需要再加一个判断selector.call !== undefined
if (isFunction(selector) && selector.call !== undefined)
this.each(function(idx) {
if (!selector.call(this, idx)) nodes.push(this);
})
else {
var excludes = typeof selector == 'string' ? this.filter(selector) :
//当selector为nodeList时执行slice.call(selector),
//注意这里的isFunction(selector.item)是为了排除selector为数组的情况
(likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector);
this.forEach(function(el) {
if (excludes.indexOf(el) < 0) nodes.push(el);
})
}
//上面得到的结果是数组,需要转成zepto对象,以便继承其它方法,实现链写
return $(nodes);
}
}

1.4 matches方法

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
    /**
* [matches 元素element是否在selector中]
* @param {[元素]} element [元素,被查询的元素]
* @param {[String]} selector [CSS选择器]
* @return {[Boolean]} [/true/false]
*/
zepto.matches = function(element, selector) {
if (!selector || !element || element.nodeType !== 1) return false
var matchesSelector = element.matches || element.webkitMatchesSelector ||
element.mozMatchesSelector || element.oMatchesSelector ||
element.matchesSelector;
//如果当前元素能被指定的css选择器查找到,则返回true,否则返回false.
//https://developer.mozilla.org/zh-CN/docs/Web/API/Element/matches
if (matchesSelector) return matchesSelector.call(element, selector);
//如果浏览器不支持MatchesSelector方法,则将节点放入一个临时div节点
var match, parent = element.parentNode,
temp = !parent;
//当element没有父节点(temp),那么将其插入到一个临时的div里面
//目的就是为了使用qsa函数
if (temp)(parent = tempParent).appendChild(element);
///将parent作为上下文,来查找selector的匹配结果,并获取element在结果集的索引
//不存在时为-1,再通过~-1转成0,存在时返回一个非零的值
match = ~zepto.qsa(parent, selector).indexOf(element);
//将插入的节点删掉(&&如果第一个表达式为false,则不再计算第二个表达式)
temp && tempParent.removeChild(element);
return match; //true/false即可
}

1.5 find方法

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
$.fn = {
find: function(selector) {
var result, $this = this;
if (!selector) {
result = $();
} //1-如果selector为node或者zepto集合时
else if (typeof selector == 'object') {
//遍历selector,筛选出父级为集合中记录的selector
result = $(selector).filter(function() {
var node = this;
//如果$.contains(parent, node)返回true,则emptyArray.some
//也会返回true,外层的filter则会收录该条记录
return emptyArray.some.call($this, function(parent) {
return $.contains(parent, node);
})
})
} else if (this.length == 1) { //2-NodeList对象,且length=1
result = $(zepto.qsa(this[0], selector));
} else {
result = this.map(function() { //3-NodeList对象,且length>1
return zepto.qsa(this, selector);
});
}
return result;
}
};

这部分会使用发daomap函数,这里给出代码
$.fn中:

1
2
3
4
5
map: function(fn) {
return $($.map(this, function(el, i) {
return fn.call(el, i, el);
}));
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.map = function(elements, callback) {
var value, values = [],
i, key;
if (likeArray(elements)) {
for (var i = 0; i < elements.length; i++) {
value = callback(elements[i], i)
if (value != null) values.push(value);
}
} else {
for (key in elements) {
value = callback(elements[key], key);
if (value != null) values.push(value);
}
}
return flatten(values);
}
1
2
3
4
//得到一个数组的副本
function flatten(array) {
return array.length > 0 ? $.fn.concat.apply([], array) : array;
}

2.selector是函数(DOMReady机制)

zepto.init中

1
2
3
if (isFunction(selector)) {
return $(document).ready(selector);
}
1
var readyRE = /complete|loaded|interactive/;

$.fn函数里面

1
2
3
4
5
6
7
8
ready: function(callback) {
if (readyRE.test(document.readyState) && document.body) {
callback($);
} else {
document.addEventListener('DOMContentLoaded', function() { callback($) }, false);
}
return this;
}

其实这里由于不考虑兼容IE10以下版本,所以写的比较简单。详细的介绍可以看我的另外的一片博客【domReady机制探究及DOMContentLoaded研究】。

这里的document.readyState顺序是:loading–>interactive(触发监听DOMContentLoaded的函数)【DOM解析完成】–>compelete【资源完成】。

ready就是为了保证回掉函数callback在onload之前执行。

第一篇博客选择符是html片段的时候,还剩下当第三个参数properties带有属性的时候没有处理。

3. 创建DOM时候带有属性

用法例如

1
$('<div></div>',{width:'100px'})

  • 工具函数/变量
1
var methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'];

在zepto.fragment中

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
zepto.fragment = function(html, name, properties) {
var dom, nodes, container;
if (singleTagRE.test(html)) {
dom = $(document.createElement(RegExp.$1));
}
if (!dom) {
//修正自闭合标签<input/>转换为<input></input>
if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>");
if (name === undefined) name = fragmentRE.test(html) && RegExp.$1;
//设置容器名,如果不是tr,tbody,thead,tfoot,td,th,则容器名为div
if (!(name in containers)) name = "*";
container = containers[name]; //创建容器
container.innerHTML = '' + html; //生成DOM
//取容器的子节点
dom = $.each(slice.call(container.childNodes), function() {
container.removeChild(this);
});
}
//TODO 第三个参数properties带有属性
if (isPlainObject(properties)) {
nodes = $(dom);
$.each(properties, function(key, value) {
// 优先获取属性修正对象,通过修正对象读写值
// methodAttributes包含'val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset',这些方法都应在`$.fn`中重写。
if (methodAttributes.indexOf(key) > -1) {
nodes[key](value);
} else {
nodes.attr(key, value)
}
});
}
return dom;
}

其中methodAttributes方法的源码实现,我们以后再具体介绍,到此为止zepto.init已经基本介绍结束了,接下来我们将另外写一篇博客看看zepto里面操作DOM的具体方法。

参考阅读:

  • Zepto核心模块源码分析
  • zepto.js 源码解析
123…7

Ruyi Zhao

70 日志
5 分类
52 标签
© 2018 Ruyi Zhao
由 Hexo 强力驱动 v3.8.0
|
主题 – NexT.Pisces v6.5.0