ZRender源碼分析2:Storage(Model層)


回顧

上一篇請移步: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漂移的方法沒有提到,以后再說吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM