• 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漂移的方法没有提到,以后再说吧
  • 相关阅读:
    hdu 6702 ^&^ 位运算
    hdu 6709 Fishing Master 贪心
    hdu 6704 K-th occurrence 二分 ST表 后缀数组 主席树
    hdu 1423 Greatest Common Increasing Subsequence 最长公共上升子序列 LCIS
    hdu 5909 Tree Cutting FWT
    luogu P1588 丢失的牛 宽搜
    luogu P1003 铺地毯
    luogu P1104 生日
    luogu P1094 纪念品分组
    luogu P1093 奖学金
  • 原文地址:https://www.cnblogs.com/hhstuhacker/p/zrender-source-storage-advance.html
Copyright © 2020-2023  润新知