ZRender源碼分析3:Painter(View層)-上


回顧

上一篇說到:ZRender源碼分析2:Storage(Model層),這次咱看來看看Painter-View層

總體理解

Painter這個類主要負責MVC中的V(View)層,負責將Storage中的shape對象繪制到canvas中,包括了:更新、渲染、變化大小、導出、修改等操作。
Painter這個類還是很明顯的構造函數,然后把方法賦值到Painter.prototype上,無新奇之處,下面為示例代碼。只有在Painter.js末尾有一個內部的createDom函數, 很明顯,傳入id,type(tagName),painter(用來確定寬高)來創建一個新的dom元素, 並且這個dom元素的寬高與painter的相同,tagname為type,絕對定位,擁有一個自定義屬性key為ata-zr-dom-id,value為id。


function Painter(root,stroage) {
	this.root = xxxx;
}

Painter.prototype.render = function () {};
Painter.prototype.refresh = function () {};
Painter.prototype.update = function () {};
Painter.prototype.clear = function () {};
.....

 /**
 * 創建dom
 * 
 * @inner
 * @param {string} id dom id 待用
 * @param {string} type dom type,such as canvas, div etc.
 * @param {Painter} painter painter instance
 */
function createDom(id, type, painter) {
    var newDom = document.createElement(type);
    var width = painter._width;
    var height = painter._height;

    // 沒append呢,請原諒我這樣寫,清晰~
    newDom.style.position = 'absolute';
    newDom.style.left = 0;
    newDom.style.top = 0;
    newDom.style.width = width + 'px';
    newDom.style.height = height + 'px';
    newDom.setAttribute('width', width * devicePixelRatio);
    newDom.setAttribute('height', height * devicePixelRatio);

    // id不作為索引用,避免可能造成的重名,定義為私有屬性
    newDom.setAttribute('data-zr-dom-id', id);
    return newDom;
}

構造函數


/**
 * 繪圖類 (V)
 * 
 * @param {HTMLElement} root 繪圖區域
 * @param {storage} storage Storage實例
 */
function Painter(root, storage) {
    this.root = root;
    this.storage = storage;

    root.innerHTML = '';
    this._width = this._getWidth(); // 寬,緩存記錄
    this._height = this._getHeight(); // 高,緩存記錄

    var domRoot = document.createElement('div');
    this._domRoot = domRoot;

    //domRoot.onselectstart = returnFalse; // 避免頁面選中的尷尬
    domRoot.style.position = 'relative';
    domRoot.style.overflow = 'hidden';
    domRoot.style.width = this._width + 'px';
    domRoot.style.height = this._height + 'px';
    root.appendChild(domRoot);

    this._domList = {};       //canvas dom元素
    this._ctxList = {};       //canvas 2D context對象,與domList對應
    this._domListBack = {};
    this._ctxListBack = {};
    
   
    this._zLevelConfig = {}; // 每個zLevel 的配置,@config clearColor
    this._maxZlevel = storage.getMaxZlevel(); //最大zlevel,緩存記錄
    // this._loadingTimer 

    this._loadingEffect = new BaseLoadingEffect({});
    this.shapeToImage = this._createShapeToImageProcessor();

    // 創建各層canvas
    // 背景
    this._domList.bg = createDom('bg', 'div', this);
    domRoot.appendChild(this._domList.bg);

    var canvasElem;
    var canvasCtx;

    /**
     * 每一個level,就是一個canvas
     *
     * DOM結構
     * root
     *   ->domRoot
     *       ->canvas level1
     *       ->canvas level2
     *       ->canvas level3
     *       ->canvas hover_level
     *
     * _domList保存所有的DOM引用
     * {
     *      1:CanvasHTMLElement
     *      2:CanvasHTMLElement
     *      3:CanvasHTMLElement
     *      hover:CanvasHTMLElement
     * }
     *
     * ctxList保存所有的canvas.getContext('2d')引用
     * {
     *      1:CanvasContext
     *      2:CanvasContext
     *      3:CanvasContext
     *      hover:CanvasContext
     * }
     */

    // 實體
    for (var i = 0; i <= this._maxZlevel; i++) {
        canvasElem = createDom(i, 'canvas', this);
        domRoot.appendChild(canvasElem);
        this._domList[i] = canvasElem;
        vmlCanvasManager && vmlCanvasManager.initElement(canvasElem); // excanvas method

        this._ctxList[i] = canvasCtx = canvasElem.getContext('2d');
        if (devicePixelRatio != 1) { 
            canvasCtx.scale(devicePixelRatio, devicePixelRatio);
        }
    }

    // 高亮
    canvasElem = createDom('hover', 'canvas', this);
    canvasElem.id = '_zrender_hover_';
    domRoot.appendChild(canvasElem);
    this._domList.hover = canvasElem;
    vmlCanvasManager && vmlCanvasManager.initElement(canvasElem); // excanvas method
    this._domList.hover.onselectstart = returnFalse;
    this._ctxList.hover = canvasCtx = canvasElem.getContext('2d');
    if (devicePixelRatio != 1) { //處理視網膜
        canvasCtx.scale(devicePixelRatio, devicePixelRatio);
    }
}


Painter.prototype._getWidth = function() {
    var root = this.root;
    var stl = root.currentStyle
              || document.defaultView.getComputedStyle(root);

    return ((root.clientWidth || parseInt(stl.width, 10))
            - parseInt(stl.paddingLeft, 10) // 請原諒我這比較粗暴
            - parseInt(stl.paddingRight, 10)).toFixed(0) - 0;

    /**
     * 這里用實際的width減去了左右的padding
     * 為什么不考慮將這兩個方法就行重載?
     */
};

Painter.prototype._getHeight = function () {
    var root = this.root;
    var stl = root.currentStyle
              || document.defaultView.getComputedStyle(root);

    return ((root.clientHeight || parseInt(stl.height, 10))
            - parseInt(stl.paddingTop, 10) // 請原諒我這比較粗暴
            - parseInt(stl.paddingBottom, 10)).toFixed(0) - 0;
};
    • 1.首先看_getWidth和_getHeight兩個方法,這是獲取當前root元素的實際寬度和高度值,詳情請看這里 獲取元素CSS值之getComputedStyle方法熟悉
    • 2.看構造,運行上篇示例,打開Chrome控制台的Element Tab,可以看到如下HTML結構:
      再看painter在內存中:

    然后咱們參照上面兩個圖,分析流程:
    • 先在指定的dom(box)元素下插入一個domRoot(跟box寬高一樣,絕對定位)
    • 在domRoot上插入一個背景div,保存到this._domList.bg變量中
    • 遍歷從storage中獲得的_maxZlevel,每層對應一個canvas元素,插入到domRoot中,保存到this.domList[遍歷序號]中,並調用每個canvas元素的getContext('2d')獲得context,保存到this._ctxList[遍歷序號]中
    • 最后處理高亮層,依舊是插入到domRoot元素中,將canvas引用和context引用存入到domList和ctxList中,不過標識都變成了hover
    • 關於視網膜屏幕請看: http://www.myexception.cn/mobile/1489709.html 與 http://www.zhangxinxu.com/wordpress/2012/10/new-pad-retina-devicepixelratio-css-page/
    • 關於vmlCanvasManager請看 IE下使用excanvas.js的注意事項
  • 3.關於加載動畫loadingEffect,暫時跳過
  • 4.關於shapeToImage,意思是將非imageShape對象轉換為ImageShape對象
    
    
    //////////////以下為zrender.js中代碼////////////////////
    /**
     * 將常規shape轉成image shape
     */
    ZRender.prototype.shapeToImage = function(e, width, height) {
        var id = guid();
        return this.painter.shapeToImage(id, e, width, height);
    };
    
    //////////////以下為Painter.js中代碼////////////////////
    Painter.prototype._createShapeToImageProcessor = function () {
        if (vmlCanvasManager) {
            return doNothing;
        }
    
        var painter = this;
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        var devicePixelRatio = window.devicePixelRatio || 1;
        
        return function (id, e, width, height) {
            return painter._shapeToImage(
                id, e, width, height,
                canvas, ctx, devicePixelRatio
            );
        };
    };
    Painter.prototype._shapeToImage = function (
        id, shape, width, height,
        canvas, ctx, devicePixelRatio
    ) {
        canvas.style.width = width + 'px';
        canvas.style.height = height + 'px';
        canvas.setAttribute('width', width * devicePixelRatio);
        canvas.setAttribute('height', height * devicePixelRatio);
    
        ctx.clearRect(0, 0, width * devicePixelRatio, height * devicePixelRatio);
    
        var shapeTransform = {
            position : shape.position,
            rotation : shape.rotation,
            scale : shape.scale
        };
        shape.position = [0, 0, 0];
        shape.rotation = 0;
        shape.scale = [1, 1];
        if (shape) {
            shape.brush(ctx, false);
        }
    
        var ImageShape = require( './shape/Image' );
        var imgShape = new ImageShape({
            id : id,
            style : {
                x : 0,
                y : 0,
                // TODO 直接使用canvas而不是通過base64
                image : canvas.toDataURL()
            }
        });
    
        if (shapeTransform.position != null) {
            imgShape.position = shape.position = shapeTransform.position;
        }
    
        if (shapeTransform.rotation != null) {
            imgShape.rotation = shape.rotation = shapeTransform.rotation;
        }
    
        if (shapeTransform.scale != null) {
            imgShape.scale = shape.scale = shapeTransform.scale;
        }
    
        return imgShape;
    };
    
    • 從zrender.js中調用過來,用了兩層閉包,有點繞,大家自行腦補,總之,最后zrender.shapeToImage(xxx)返回的是一個ImageShape對象
    • 在painter的構造函數中有_createShapeToImageProcessor的調用,直接指向了this.shapeToImage,這說明_shapeToImage只是一個內部方法
    • 在_createShapeToImageProcessor中,我們發現,如果用的是excanvas(IE678),那么不支持這個特性,return掉(這是在API沒有公開這個接口的原因?)
    • 如果不是excanvas,自行創建一個canvas元素,獲取其context對象,然后傳給_shapeToImage,饒了半天,最后Painter._shapeToImage才是苦力工啊
    • 在_shapeToImage中,首先將canvas的寬高設置成指定的寬高,然后清除畫布,保存變形參數,再將變形參數重置,調用 shape的brush方法進行繪制,此時,已經完成了新canvas的創建,然后再畫上指定的shape
    • 新建一個ImageShape,將image設置為以前新建的canvas.toDataURL() 關於canvas與Image互換,請看:http://www.jb51.net/html5/97104.html
    • 最后把之前shape的變形參數設置到ImageShape上
    • 既然API中不公開這個接口,其他地方也沒調用,作者是個啥意圖呢?

關於完美繼承


/**
 * 構造類繼承關系
 * 
 * @param {Function} clazz 源類
 * @param {Function} baseClazz 基類
 */
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.constructor = clazz;
}

因為接下來的講解之中,在loadingEffect和Shape對象中,都會有JS繼承的出現,zrender/tool/util.js中有一個inherits的方法,實現了完美繼承。 有興趣的同學們可以看看下面兩個,我就不詳細的說了。

  • http://blog.csdn.net/justoneroad/article/details/7327805
  • http://my.oschina.net/antianlu/blog/228267

結束

因為Painter的內容牽扯較多,關於Shape對象不詳細說道說道又無法進行,說以下篇咱們看看Shape到底是怎么組織的,等下下篇,再從來Painter類


免責聲明!

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



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