回顧
上一篇請移步:zrender源碼分析1:總體結構
本篇進行ZRender的MVC結構中的M進行分析
總體理解
上篇說到,Storage負責MVC層中的Model,也就是模型,對於zrender來說,這個model就是shape對象,在1.x表現的還不強烈,到了2.x, 在zr.addShape()的時候,傳入的參數就必須是new出來的對象了詳情請看這里 2.x相比1.x的變化 ,關於這個變化多說點吧,那就是從1.x升級到2.x的時候,因為方式變了,總不能改掉所有的代碼,總不能像ext一樣, (從ExtJS3升級到ExtJS4是一個特別痛苦的過程),所以我們在原有的可視化程序中,加入了如下helper(該程序基於ExtJS5)
Ext.define('Nts.Utils.ChartHelper', {
singleton: true,
shapeMap: {},
requireMap: {},
/**
* 通過shape的類型獲得shape的構造函數
* 由於zrender的升級,所以導致該方法的出現,詳情
* see:https://github.com/ecomfe/zrender/wiki/2.x%E7%9B%B8%E6%AF%941.x%E7%9A%84%E5%8F%98%E5%8C%96
*
* @param shapeType shape類型
* @returns {Constructor}
*/
getShapeTypeConstructor: function (shapeType) {
// 由於zrender2.0的addShape時不能add對象,只能add一個初始化好的shape類,
// 所以每次都需要require加載所需的類,在這里,shapeMap是一個緩存對象
// 因為echarts包含了requirejs的源碼,但是沒有將define和require方法暴露出來
// 迫不得已修改了echarts的源代碼,window.require = require;
if (!this.shapeMap[shapeType]) {
this.shapeMap[shapeType] = require('zrender/shape/' + Ext.String.capitalize(shapeType));
}
return this.shapeMap[shapeType];
},
/**
* 根據shape類型和傳入shape的參數,新建shape類,返回的結果可以直接被addShape
*
* 該方法有多個重載,如下
*
* 1.Nts.Utils.ChartHelper.makeShapeInstance('image',{scale:[1,2],hover:....});
* 2.Nts.Utils.ChartHelper.makeShapeInstance({shape:'image',scale:[1,2],hover:....});
*
* 第2中方式為zrender1.x中兼容的方式,其中shape屬性可以是 shape|shapeType|type
*
* @param shapeType shape類型
* @param option 參數
* @returns {Object} shape對象
*/
makeShapeInstance: function (shapeType, option) {
if (Ext.isObject(shapeType)) {
option = shapeType;
shapeType = option.shape || option.shapeType || option.type
}
var ctor = this.getShapeTypeConstructor(shapeType);
if (!ctor) new Error('cannot find this shape in zrender');
return new ctor(option);
}
});
這樣一來,就能夠繼續像之前一樣愉快的玩耍了。言歸正傳,把代碼全部折疊起來,我們來看看總體的結構。
還好還好,這里的結構還是超級簡單。
- 1.這是個典型的JS創建對象的結構, var Storage = function () {}; Storage.prototype.add = function () {.....};
- 2.方法附加在protype上,屬性寫在構造函數里,每個附加到prototype的方法都返回this,支持鏈式調用
- 3.Storage n.貯存; 貯藏; 儲藏處,倉庫; 貯存器,蓄電(瓶); 維護所有的shape,可以通過其中的一些屬性進行查看
下面,咱們來逐個擊破。
構造函數
二話不說,先貼代碼
/**
* 內容倉庫 (M)
*
*/
function Storage() {
// 所有常規形狀,id索引的map
this._elements = {};
// 所有形狀的z軸方向排列,提高遍歷性能,zElements[0]的形狀在zElements[1]形狀下方
this._zElements = [];
// 高亮層形狀,不穩定,動態增刪,數組位置也是z軸方向,靠前顯示在下方
this._hoverElements = [];
// 最大zlevel
this._maxZlevel = 0;
// 有數據改變的zlevel
this._changedZlevel = {};
}
作者都注釋了,這是個內容倉庫,又想想,這不就是相當於糧倉嘛,shape對象就是一個一個的糧食。構造函數里的_elements,_zElement,_hoverElements就是糧倉。 而_elements和_zElements這兩個變量其實存入的是一樣的東西,只是存入的方式不太相同而已。其中,zElement這個變量中的z,大概就是zlevel(分層)的意思, 我想這便是zrender的最核心的思想,分層繪圖。接下來咱們用一個取(bei)巧(bi)的方式,來看看內存中的呈現。打開zrender.js,加入一行代碼:window.z = this;
function ZRender(id, dom) {
this.id = id;
this.env = require('./tool/env');
this.storage = new Storage();
this.painter = new Painter(dom, this.storage);
this.handler = new Handler(dom, this.storage, this.painter);
window.z = this; // 把z透漏出去
// 動畫控制
this.animatingShapes = [];
this.animation = new Animation({
stage : {
update : getAnimationUpdater(this)
}
});
this.animation.start();
}
然后,運行如下示例:
require(['../src/zrender',
'../src/shape/Image',
'../src/shape/Text',
'../src/shape/Circle'],
function (zrender, ImageShape, TextShape, CircleShape) {
var box = document.getElementById('box');
var zr = zrender.init(box);
zr.addShape(new CircleShape({
style: {
x: 120,
y: 120,
r: 50,
color: 'red'
},
hoverable: true
}));
zr.addShape(new TextShape({
style: {
x: 220,
y: 220,
color: 'red',
text: 'something text'
},
hoverable: true,
zlevel: 2
}));
zr.render();
});
最后,在控制台中輸入z,回車,看到如下打印:
可以很明顯的看到,_elements里的東西,是直接塞入的,不管什么順序,而zElements里的東西,是按照shape對象的zlevel進行存放的,具體怎么維護,就要看怎么增刪改查了
PS:這張圖比較重要,在下面增刪改查的時候,可以詳盡的表現出其過程
增
/**
* 添加
*
* @param {Shape} shape 參數
*/
Storage.prototype.add = function (shape) {
shape.updateNeedTransform();
shape.style.__rect = null;
this._elements[shape.id] = shape;
this._zElements[shape.zlevel] = this._zElements[shape.zlevel] || [];
this._zElements[shape.zlevel].push(shape);
this._maxZlevel = Math.max(this._maxZlevel, shape.zlevel);
this._changedZlevel[shape.zlevel] = true;
/**
* _elements ->
* {
* _zrender_101_: shapeObject,
* _zrender_102_: shapeObject,
* _zrender_103_: shapeObject,
* ...
* }
*
* _zrender_103_ 為guid生成的
*
* _zElements ->
* {
* 1: [shapeObject,shapeObject],
* 2: [shapeObject,shapeObject....],
* 3. [...]
* }
*
* 123 為層數
*
* _maxZlevel: 3
* changedZlevel: {1:true,2:true....}
*/
return this;
};
/**
* 添加高亮層數據
*
* @param {Object} params 參數
*/
Storage.prototype.addHover = function (params) {
/**
* 這里判斷了一大推參數,來預處理是否需要變形,變形金剛(Transformers)
* 豆瓣電影:http://movie.douban.com/subject/7054604/
* 在最初添加的時候,處理變形開關,就不用在用到的時候重新做了
*/
if ((params.rotation && Math.abs(params.rotation[0]) > 0.0001)
|| (params.position
&& (Math.abs(params.position[0]) > 0.0001
|| Math.abs(params.position[1]) > 0.0001))
|| (params.scale
&& (Math.abs(params.scale[0] - 1) > 0.0001
|| Math.abs(params.scale[1] - 1) > 0.0001))
) {
params.needTransform = true;
}
else {
params.needTransform = false;
}
this._hoverElements.push(params); //簡單的將高亮層push到_hoverElements中
return this;
};
- 1._elements是以id為key,shape對象為value,進行存儲
- 2._zElements是一個數組,以level為數組下標,同一個level的shape對象集合組成數組為值(如果該層沒有初始化,會有一個初始化的過程)
- 3.每次add,都會重置_maxZlevel變量,它始終表示最大的level;_changedZlevel是一個對象,表示變動的level(如果變動,在painter中會進行重繪)
- 4.addHover的時候,先預處理needTransform參數,之后,將shape對象直接塞入_hoverElements數組,不做復雜處理
刪
/**
* 刪除高亮層數據
*/
Storage.prototype.delHover = function () {
this._hoverElements = [];
return this;
};
/**
* 刪除,shapeId不指定則全清空
*
* @param {string= | Array} idx 唯一標識
*/
Storage.prototype.del = function (shapeId) {
if (typeof shapeId != 'undefined') {
var delMap = {};
/**
* 處理各種重載
* 1.如果不是個數組,直接加入到delMap中
* 2.如果是個數組,遍歷之
*/
if (!(shapeId instanceof Array)) {
// 單個
delMap[shapeId] = true;
}
else {
// 批量刪除
if (shapeId.lenth < 1) { // 空數組
return;
}
for (var i = 0, l = shapeId.length; i < l; i++) {
delMap[shapeId[i].id] = true;
}
}
var newList;
var oldList;
var zlevel;
var zChanged = {};
for (var sId in delMap) {
if (this._elements[sId]) {
zlevel = this._elements[sId].zlevel;
this._changedZlevel[zlevel] = true;
/**
* 這里主要處理zElements中元素的刪除
* 這里確認每一個zlevel只遍歷一次,因為一旦進入這個if,在if的末尾,就會將flag設置為false,下次就進不來
*
* 1.遍歷delMap,取出單個shape的zlevel,然后從_zElements[zlevel] 取出所有,命名為oldList
* 2.遍歷oldList,如果delMap中沒有當前遍歷的shape,就加入到newList,最后該層的_zElements[zlevel]就是newList
* 3.設置標志位,使之為false,表示該層已經被處理,就不要再次處理了
*/
if (!zChanged[zlevel]) {
oldList = this._zElements[zlevel];
newList = [];
for (var i = 0, l = oldList.length; i < l; i++){
if (!delMap[oldList[i].id]) {
newList.push(oldList[i]);
}
}
this._zElements[zlevel] = newList;
zChanged[zlevel] = true;
}
//將shape從_elements中刪除
delete this._elements[sId];
}
}
}
else{
// 不指定shapeId清空
this._elements = {};
this._zElements = [];
this._hoverElements = [];
this._maxZlevel = 0; //最大zlevel
this._changedZlevel = { //有數據改變的zlevel
all : true
};
}
return this;
};
- 1.delHover方法很是簡單,將_hoverElements中的東西清空,返回this
- 2.關於del方法,如果不傳入shapeId,會將所有的shape都刪除,全部倉庫變量清空,all:true,就是表示所有層重繪
- 3.對參數的重載進行處理,如果是數組,遍歷之
- 4.shapeId instanceof 在某種情況下,會有問題的吧?為啥不用 Object.prototype.toString.call(xxx) === '[object Array]',為了可讀性?
- 5.對於_elements中的刪除,一句delete this._elements[sId];搞定,但是對於_zElements,就要費一番功夫了,具體移步代碼中的注釋吧
改
/**
* 修改
*
* @param {string} idx 唯一標識
* @param {Object} params 參數
*/
Storage.prototype.mod = function (shapeId, params) {
var shape = this._elements[shapeId];
if (shape) {
shape.updateNeedTransform();
shape.style.__rect = null;
this._changedZlevel[shape.zlevel] = true; // 可能修改前后不在一層
/**
* 將參數合並,params && util.merge(shape, params, true);
*
* this._changedZlevel[shape.zlevel] = true; 這里是為了防范:
*
* var imageShape = new ImageShape({src:'xxx.png',zlevel:1});
* imageShape.mod({zlevel:3});
*
* 這里就是:level1和level3都變化了,_maxZlevel也變化了。
*/
if (params) {
util.merge(shape, params, true);
}
this._changedZlevel[shape.zlevel] = true; // 可能修改前后不在一層
this._maxZlevel = Math.max(this._maxZlevel, shape.zlevel);
}
return this;
};
- 1.updateNeedTransform這個方法,也是預處理變形金剛的問題
- 2.為了防止修改shape對象時不在同一層的問題,在前后都執行了this._changedZlevel[shape.zlevel] = true;雖然很羅嗦,但也很必要
- 3.util.merge的作用是將新加入的params合並到原來的參數中,具體代碼就不再羅嗦了
- 4.最后重置_maxZlevel,在z軸遍歷的時候,確保索引。
查
/**
* 遍歷迭代器
*
* @param {Function} fun 迭代回調函數,return true終止迭代
* @param {Object=} option 迭代參數,缺省為僅降序遍歷常規形狀
* hover : true 是否迭代高亮層數據
* normal : 'down' | 'up' | 'free' 是否迭代常規數據,迭代時是否指定及z軸順序
*/
Storage.prototype.iterShape = function (fun, option) {
/**
* 處理默認情況 option = option ||{ hover: false, normal: 'down'};
*/
if (!option) {
option = {
hover: false, //不遍歷高亮層
normal: 'down' //高層優先
};
}
if (option.hover) {
//高亮層數據遍歷
for (var i = 0, l = this._hoverElements.length; i < l; i++) {
if (fun(this._hoverElements[i])) {
return this;
}
}
}
var zlist;
var len;
if (typeof option.normal != 'undefined') {
//z軸遍歷: 'down' | 'up' | 'free'
switch (option.normal) {
case 'down':
// 降序遍歷,高層優先
var l = this._zElements.length;
while (l--) {
zlist = this._zElements[l];
if (zlist) {
len = zlist.length;
while (len--) {
if (fun(zlist[len])) {
return this;
}
}
}
}
break;
case 'up':
//升序遍歷,底層優先
for (var i = 0, l = this._zElements.length; i < l; i++) {
zlist = this._zElements[i];
if (zlist) {
len = zlist.length;
for (var k = 0; k < len; k++) {
if (fun(zlist[k])) {
return this;
}
}
}
}
break;
// case 'free':
default:
//無序遍歷
for (var i in this._elements) {
if (fun(this._elements[i])) {
return this;
}
}
break;
}
}
return this;
};
/**
* 根據指定的shapeId獲取相應的shape屬性
*
* @param {string=} idx 唯一標識
*/
Storage.prototype.get = function (shapeId) {
return this._elements[shapeId];
};
Storage.prototype.getMaxZlevel = function () {
return this._maxZlevel;
};
Storage.prototype.getChangedZlevel = function () {
return this._changedZlevel;
};
Storage.prototype.clearChangedZlevel = function () {
this._changedZlevel = {};
return this;
};
Storage.prototype.setChangedZlevle = function (level) {
this._changedZlevel[level] = true;
return this;
};
Storage.prototype.hasHoverShape = function () {
return this._hoverElements.length > 0;
};
- 1.iterShape分為三種遍歷的方式(無序free,從上至下down,從下至上up),有一個開關(是否遍歷高亮層hover)
- 2.如果沒有指定option,設置默認值,不遍歷高亮層,從上至下遍歷
- 3.如果需要遍歷高亮層,遍歷_hoverElements數組,調用回調函數fun,如果fun的返回值能轉化為true,直接return掉了(多說一句,不知可否像jQuery的each一樣,是false的時候再return,就不用每次在函數末尾return false了?)
- 4.如果down和up的時候,遍歷的是_zElemements數組,因為層數可能是間隔的,所以每次取出,都會判斷一下是否為undefined,如果有值,遍歷里面的數組,執行fun回調,return的邏輯跟上一條一樣。
- 5.如果是無序遍歷,最好辦,遍歷_elements數組,進行調用fun
- 6.至於get(通過id獲取shape對象)/getMaxZlevel(獲取最大層級)/getChangedZlevel(獲取改變的層級對象)/clearChangedZlevel(清空層級變化)/setChangedZlevle(設置某個層級變化為true)/hasHoverShape(是否存在高亮層)都比較簡單,就不詳述了
總結
- 1.其實這個Storage很好理解,主要是對Shape對象進行一些增刪改查的封裝(封裝的好處我就不說了,自行腦補吧)
- 2.可見作者很是理解我們這些新手,代碼寫的相當易懂,我喜歡(恨死了jQuery了),自行猜測,不要噴我哦
- 3.還有一個drift漂移的方法沒有提到,以后再說吧