vieyahn2017 / iBlog

44 stars 0 forks source link

10.31 zrender.js初步学习,源码阅读 #232

Closed vieyahn2017 closed 5 years ago

vieyahn2017 commented 6 years ago

zrender.js初步学习

vieyahn2017 commented 6 years ago

zrender源码分析1:总体结构 https://www.cnblogs.com/hhstuhacker/p/zrender-source-advance-frame.html

开始

zrender(Zlevel Render) 是一个轻量级的Canvas类库,这里是GitHub的网址 点我, 类似的类库有Kinetic.JS、EaselJS。 但貌似都没有zrender好用(可能是更加符合国人的习惯),强大的图表工具echarts就是在zrender基础上建立, 用zrender和echarts做了两个关于canvas的两个可视化项目之后,忍不住看了下zrender的项目代码(也有需要修改源代码的缘故), 但是翻开之后,代码的结构比较清晰,注释也都是中文,比较容易读懂。最大的感想就是,沒有像jQuery之类的对于代码极(bian)度(tai)的精简追求,各种意图一看便知。 如果对于zrender的api不熟悉,请移步github上,这里只对源码进行了一些详(qian)细(xian)的分析。

总体结构

关于总体结构,最贴切的描述恐怕也没这一张图来的爽: zrender结构图 从github将项目clone下来之后,打开src/zrender.js之后,有如下发现:

var _instances = {};    //ZRender实例map索引

var zrender = {};
zrender.version = '2.0.0';

/**
 * zrender初始化
 * 不让外部直接new ZRender实例,为啥?
 * 不为啥,提供全局可控同时减少全局污染和降低命名冲突的风险!
 *
 * @param {HTMLElement} dom dom对象,不帮你做document.getElementById了
 * @param {Object=} params 个性化参数,如自定义shape集合,带进来就好
 *
 * @return {ZRender} ZRender实例
 */
zrender.init = function(dom, params) {
    var zi = new ZRender(guid(), dom, params || {});
    _instances[zi.id] = zi;
    return zi;
};

我想这里已经很明显,用zrender.init(dom)初始化的时候,直接new一个内部的ZRender对象进行返回,原因我想作者已经写的很明白了(提供全局可控同时减少全局污染和降低命名冲突的风险!) 总比每次new ZRender()好多了,最起码看起来是这样,并且把每个实例就行保存,便于维护。 至于周围的其他的三个方法,dispose,getInstance,delInstance 就没什么可说的了,不过在项目中,也怎么用得上。

/**
 * zrender实例销毁,记在_instances里的索引也会删除了
 * 管生就得管死,可以通过zrender.dispose(zi)销毁指定ZRender实例
 * 当然也可以直接zi.dispose()自己销毁
 *
 * @param {ZRender=} zi ZRender对象,不传则销毁全部
 */
zrender.dispose = function (zi) {
    if (zi) {
        zi.dispose();
    }
    else {
        for (var key in _instances) {
            _instances[key].dispose();
        }
        _instances = {};
    }

    return zrender;
};

/**
 * 获取zrender实例
 *
 * @param {string} id ZRender对象索引
 */
zrender.getInstance = function (id) {
    return _instances[id];
};

/**
 * 删除zrender实例,ZRender实例dispose时会调用,
 * 删除后getInstance则返回undefined
 * ps: 仅是删除,删除的实例不代表已经dispose了~~
 *     这是一个摆脱全局zrender.dispose()自动销毁的后门,
 *     take care of yourself~
 *
 * @param {string} id ZRender对象索引
 */
zrender.delInstance = function (id) {
    delete _instances[id];
    return zrender;
};

\\接下来就是核心的ZRender构造函数,这里可以很清晰的看到M(Storage)V(Painter)C(Handler)的结构.

/**
 * ZRender接口类,对外可用的所有接口都在这里!!
 * storage(M)、painter(V)、handler(C)为内部私有类,外部接口不可见
 * 非get接口统一返回支持链式调用~
 *
 * @param {string} id 唯一标识
 * @param {HTMLElement} dom dom对象,不帮你做document.getElementById
 *
 * @return {ZRender} ZRender实例
 */
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);

    // 动画控制
    this.animatingShapes = [];
    this.animation = new Animation({
        stage : {
            update : getAnimationUpdater(this)
        }
    });
    this.animation.start();
}

Storage只是JS对象级别的对于Shape图形的增(add/addHover)删(del,delHover)改(mod)查(get/iterShape/getMaxZlevel等),更像一个数据结构的东西 Painter负责真正的绘图操作,这里是比较繁重的部分 1.负责canvas及其周边DOM元素的创建与处理 2.负责调用各个Shape(预定义好的)进行绘制 3.提供基本的操作方法,渲染(render)、刷新(refresh)、尺寸变化(resize)、擦除(clear)等 Handler负责事件处理,解决基础的浏览器兼容问题、进行事件的注册与转发、拖动 至于附加在ZRender的prototype的其他方法,除了关于动画部分的,其他的都是调用的Storage、Painter、Handler,这里就不再赘述了。

vieyahn2017 commented 6 years ago

ZRender源码分析2:Storage(Model层)
https://www.cnblogs.com/hhstuhacker/p/zrender-source-storage-advance.html
ZRender源码分析3:Painter(View层)-上
https://www.cnblogs.com/hhstuhacker/p/zrender-source-painter-part1.html
ZRender源码分析4:Painter(View层)-中
https://www.cnblogs.com/hhstuhacker/p/zrender-source-painter-part2.html
ZRender源码分析5:Shape绘图详解
https://www.cnblogs.com/hhstuhacker/p/zrender-source-painter-shape.html
ZRender源码分析6:Shape对象详解之路径
https://www.cnblogs.com/hhstuhacker/p/zrender-source-painter-shape-path.html

目前zrender已经更新到4的版本了, 上述帖子多多少少都有些过时了。

vieyahn2017 commented 6 years ago

官方文档 https://ecomfe.github.io/zrender-doc/public/api.html#zrender-api

vieyahn2017 commented 6 years ago

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

2017年01月11日 10:41:23 TechFE 阅读数:9688 标签: zrender ECharts 个人分类: ECharts zrender 版权声明:本文为博主原创文章,转载请注明出处和原文链接。 https://blog.csdn.net/future_todo/article/details/54341386 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文件下,我们稍后会详细解释,现在我们大概来看下它们分别的作用。

源码结构

目录的介绍

2.入口(zrender.js)

2.1 初始化

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

var zr = zrender.init(document.getElementById('main'));
\\源码:

    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的管理机制。

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中查看,这里只将方法名放在这里。

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机制处理。 使用示例:

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)。

   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没有提供监听每个元素的机制,这就需要一些处理。处理的思路是:监听事件的作用坐标(如点击时候的坐标),判断在哪个绘制元素的范围中,如果在某个元素中,这个元素就监听该事件。具体的思路可以查看参考阅读给的链接文章。

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

3.3 V–视图层(Painter.js)

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

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

4.设计模式总结

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

4.1 AMD模式

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

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

   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是个自己定义的属性
   }

另外不要忘了,在构造函数中应该重写父类的属性。

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

调用

zrUtil.inherits(Displayable, Element);

4.3 混入模式

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

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;
}

调用

zrUtil.mixin(Displayable, RectText);

4.4 jquery的extend模式

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

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)混入方法

util.mixin(Handler, Eventful); 包括一下几种方法

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)
vieyahn2017 commented 6 years ago

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

2017年01月11日 10:45:36 TechFE 阅读数:5198 标签: ECharts zrender 个人分类: ECharts zrender 版权声明:本文为博主原创文章,转载请注明出处和原文链接。 https://blog.csdn.net/future_todo/article/details/54341426 上一篇介绍了zrender的总体结构,这一篇我们就详细介绍View层–Painter(Painter.js)。

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

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

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

1.渲染结构分析

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

<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画布。

sector.on('mouseover', function() {
    zr.addHover(this, {
        stroke: 'yellow',
        lineWidth: 10,
        opacity: 1
    });
    zr.refresh();
});
sector.on('mouseout', function() {
    zr.removeHover(this);
});
<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.构造函数

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

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刷新,刷新去绘制

/**
* 刷新
* @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
_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数组),所以后面的只针对单个元素绘制即可。

//... ...
for (var i = 0, l = list.length; i < l; i++) {
   //... ...
   this._doPaintEl(el, currentLayer, paintAll, scope);//绘制每个元素
}
_doPaintEl
//... ...
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)
addHover: function(el, style) {
    if (this.painter.addHover) {
        this.painter.addHover(el, style);
        this.refreshHover();
    }
}
addHover(Painter.js)
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)
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();
}

参考阅读:

阅读更多 想对作者说点什么? 我来说一句

vieyahn2017 commented 6 years ago

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

2017年01月11日 10:48:15 TechFE 阅读数:2563 标签: ECharts zrender 个人分类: ECharts zrender 版权声明:本文为博主原创文章,转载请注明出处和原文链接。 https://blog.csdn.net/future_todo/article/details/54341458 这一篇,介绍下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)中的调用

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

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,针对目标图形元素触发事件。

/**
 * 事件分发代理
 *
 * @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(拖动事件)

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

2.canvas上元素的监听事件

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

 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判断点在哪个元素中。

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是否在图形的包围盒上。

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方法

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中,其实已经为每个元素生成了它的包围盒上。

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。