zoukankan      html  css  js  c++  java
  • 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漂移的方法没有提到,以后再说吧
  • 相关阅读:
    PostgreSQL 语法
    Linux 上安装 PostgreSQL
    Gitlab基础知识介绍
    Grafana 入门知识介绍
    数据卷容器
    Docker网络详解——原理篇
    Docker网络详细理解-容器网络互通
    搭建Elasitc stack集群需要注意的日志问题
    创建Elasticsearch集群并为它们配置TLS安全通信
    Elastic:为Elastic Docker部署设置安全
  • 原文地址:https://www.cnblogs.com/hhstuhacker/p/zrender-source-storage-advance.html
Copyright © 2011-2022 走看看