js模版深度解析


js模版對於一個健壯的組件庫來說,至關重要。猶如建築一棟大樓,模版就是鋼筋,數據就是水泥,事件就是布線和弱電。本文將從一個小函數講起,然后重點探討js模版的實現模式、易用性、可擴展性,然后再對ext的模版體系做簡單分析。

 

由於工作原因,本人一直在維護一個datagrid組件,datagrid的需求千變萬化,大概60%的需求都是對單元格的處理,剛剛開始的時候需要一個功能就加一個功能,比如單元格需要根據數據改變背景顏色,於是我便在表格生成之后直接操作dom,比如帶checkbox的datagrid,翻頁后需要保存已選狀態,於是我便在表格生成之后查找checkbox然后再選中。需要在增加,datagrid也慢慢變的臃腫起來,不堪重負,leader也決定開始重構了。在重構之初,我便決定,在表格生成之前把需要處理的都完成,這樣就可以節省查詢dom的時間。這樣以來,前期創建需要處理的邏輯就會很多,所以這里就需要一個很完善的模版體系來做支持,否則玩到最后又會變的很臃腫。

 

於是我嘗試着寫了一個簡單的基於對象模式的模版,代碼如下:

 

/**
 *
 *  對象模式創建模版
 *
 *  @param {Array} attrs 生成的節點數組
 *         @param {String} type 類型
 *         @param {Array|Object} attr 屬性
 *         @param {Array|Object} child  子節點
 *         @param {Number} num  子節生成個數
 *         @param {Function} func  處理函數
 *         @param {Array} data  數據
 *
 *     @param {Element|String} target
 */
var tpl = function(ats, target) {
    target = fast.id(target);
    if (fast.isArray(ats) && ats.length > 0 && target.appendChild) {
        for (var i = 0, len = ats.length; i < len; i++) {
            var attrs = ats[i], tag = attrs.tag, attr = attrs.attr || {}, data = attrs.data, func = attrs.func, child = attrs.child, num = attrs.num ? attrs.num : 1, j = 0;
            var fragment = document.createDocumentFragment();
            for (; j < num; j++) {
                var isFunc = false;
                if (data) {
                    if (child) {
                        if (fast.isArray(child)) {
                            for (var k = 0, l = child.length; k < l; k++) {
                                child[k].data = data[j];
                            }
                        } else {
                            child.data = data[j];
                        }
                    } else {
                        if (func) {
                            attr = func(j, attr, data);
                            isFunc = true;
                        } else {
                            data = fast.values(data);
                            attr.text = data[j];
                        }
                    }
                }
                (isFunc === false) && func && ( attr = func(j, attr, data));
                var nodes = fast.node(tag, attr);
                fragment.appendChild(nodes);
                child && tpl(child, nodes);
            }
            target.appendChild(fragment);
        }
    }
};

 

另外創建了一個基類,這個基類后面的例子都會用到,希望讀者注意。

View Code
var doc = window.document, _toString = Object.prototype.toString;

var fast = {
    isString : function(obj) {
        return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
    },
    isNumber : function(obj) {
        return _toString.call(obj) === '[object Number]';
    },
    isArray : [].isArray ||
    function(obj) {
        return _toString.call(obj) === '[object Array]';
    },
    isObject : function(obj) {
        return obj == null ? String(obj) == 'object' : _toString.call(obj) === '[object Object]' || true;
    },
    isEmptyObject : function(obj) {
        for (var name in obj) {
            return false;
        }
        return true;
    },
    getID : function() {
        var num1 = new Date().getTime();
        var num2 = parseInt(Math.random() * 100000, 10);
        return num1 + num2;
    },
    id : function(id) {
        if (this.isString(id)) {
            return doc.getElementById(id);
        } else if (id.nodeType) {
            return id;
        }
        return;
    },
    html : function(el, html) {
        el = this.id(el);
        if (html) {
            if (el != null && 'innerHTML' in el) {
                el.innerHTML = html;
            }
        } else {
            return el.innerHTML;
        }
    },
    values : function(obj) {
        var ret = [];
        for (var key in obj) {
            ret.push(obj[key]);
        }
        return ret;
    },
    setCssText : function(el, cssText) {
        el.style.cssText = cssText;
    },
    setAttr : function(element, attrObj) {
        var me = this, mapObj = {
            "class" : function() {
                element.className = attrObj["class"];
            },
            "style" : function() {
                me.setCssText(element, attrObj["style"]);
            },
            "text" : function() {
                if (attrObj["text"].nodeType) {
                    element.appendChild(attrObj["text"]);
                } else {
                    element.appendChild(document.createTextNode(attrObj["text"]));
                }
            }
        }
        for (p in attrObj) {
            if (mapObj[p]) {
                mapObj[p]();
            } else {
                element.setAttribute(p, attrObj[p]);
            }
        }
    },
    node : function(type, attrObj) {
        var element = doc.createElement(type);
        if (!this.isEmptyObject(attrObj)) {
            this.setAttr(element, attrObj);
        }
        return element;
    },
    testTime : function(get_as_float) {
        var now = new Date().getTime() / 1000;
        var s = parseInt(now, 10);
        return (get_as_float) ? now : (Math.round((now - s) * 1000) / 1000) + ' ' + s;
    },
    //ext**********************************/
    _indexOf : Array.prototype.indexOf,
    inArray : function(elem, arr, i) {
        var len;
        if (arr) {
            if (this._indexOf) {
                return this._indexOf.call(arr, elem, i);
            }
            len = arr.length;
            i = i ? i < 0 ? Math.max(0, len + i) : i : 0;
            for (; i < len; i++) {
                if ( i in arr && arr[i] === elem) {
                    return i;
                }
            }
        }
        return -1;
    },
    isDate : function(o) {
        return (null != o) && !isNaN(o) && ("undefined" !== typeof o.getDate);
    },
    Format : {},
    decodeHTML : function(str) {
        str = String(str).replace(/&quot;/g, '"').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, "&");
        //處理轉義的中文和實體字符
        return str.replace(/&#([\d]+);/g, function(_0, _1) {
            return String.fromCharCode(parseInt(_1, 10));
        });
    },
    apply : function(object, config, defaults) {
        if (defaults) {
            this.apply(object, defaults);
        }
        var enumerables = true, enumerablesTest = {
            toString : 1
        };
        for (i in enumerablesTest) {
            enumerables = null;
        }
        if (enumerables) {
            enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'toString', 'constructor'];
        }
        if (object && config && typeof config === 'object') {
            var i, j, k;

            for (i in config) {
                object[i] = config[i];
            }

            if (enumerables) {
                for ( j = enumerables.length; j--; ) {
                    k = enumerables[j];
                    if (config.hasOwnProperty(k)) {
                        object[k] = config[k];
                    }
                }
            }
        }

        return object;
    }
};

 

在模版使用之前,我們需要預先定義一組數據,這組數據后面的幾個模版體系都會用到

View Code
var data = [
{name : "test1",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "1"}, 
{name : "test2",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "2"}, 
{name : "test3",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "3"}, 
{name : "test4",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "4"}, 
{name : "test5",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "5"}, 
{name : "test6",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "6"}, 
{name : "test7",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "7"}, 
{name : "test8",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "8"}, 
{name : "test9",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "9"}, 
{name : "test10",sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : "10"}
];
for(var i = 10; i < 1000; i++){
    data.push({name : "test"+i,sex : "man",age : "20",date : "2011-10-13 12:00:00:0",uid : i});
}

 

這個模版的使用事例如下:

var td = [{tag:"td",num:5,attr:{text:"text"}}];
var tr = [{tag:"tr",num:1000,child:td,data:data}];
var tbody = [{tag:"tbody",child:tr}];
tpl([{tag:"table",attr:{style:"width:100%",border:"1"},child:tbody}],"example");

當然,您也可以這樣寫:

tpl([{tag:"table",attr:{style:"width:100%",border:"1"},
    child:[{tag:"tbody",
        child:[{tag:"tr",num:1000,
            child:[{tag:"td",num:5,attr:{text:"text"}}],
        data:data}]}
        ]}],
    "example");

 

該模版的核心思路就是遞歸創建dom,支持對每個dom綁定數據,支持外部函數調用(helper),支持內嵌數據處理,支持一次創建多個平級dom。


對於一個組件庫來說,感覺這樣很完美了,於是我興致沖沖的想拿其他的模版體系做對比。找了一大圈,發現別人玩模版不是這樣玩的,大部分都是先拼裝字符串,然后再放到一個閉包里來處理,再返回。

於是我模仿着別人寫了一個原型,代碼如下:

 

/*
 *  字符串模式創建模版
 *
 */
var tp = function(str, data) {
    var str = fast.id(str) ? fast.html(str) : str, str = str.replace(/<\#(\s|\S)*?\#>/g, function(p) {
        return p.replace(/("|\\)/g, "\\$1").replace("<#", '_s.push("').replace("#>", '");').replace(/<\%([\s\S]*?)\%>/g, '",$1,"')
    }).replace(/\r|\n/g, ""), keys = [], values = [], i;
    for (i in data) {
        keys.push(i);
        values.push(data[i]);
    }
    return (new Function(keys, "var _s=[];" + str + " return _s;") ).apply(null, values).join("");
};

調用方式大致如下:

<div id="tptest" style="height:100px;overflow-y: auto"></div>
<script id="t1" type="text/tpl">
<#<table width="100%" border="1">#>
    for (var i = 0, l = list.length; i < l; i ++) { 
        <#<tr>#>
            for(var p in list[i]){
                <#<td>
                    <%list[i][p]%>
                </td>#>
            }
        <#</tr>#>
    }
<#</table>#>
</script>
<script>
var tpdata = {
    list: data
};
fast.html("tptest",tp("t1",tpdata));

 

做了一下性能對比,乖乖,這個性能比對象模式更快,而且對象模式能實現的,這個基本都能實現。但是對於處理單個dom的方式上,總感覺缺點什么,想來想去,原來這種方式不能把一個dom拿出來單獨玩,需要跳到模版里面去,這里就需要注意環境變量以及邏輯關系了。

 

還是不死心,於是一狠心把ext的模版抽筋剝皮拿了出來,代碼如下(運行需要上面的fast基類,未經詳細測試,不建議用於生產環境):

View Code
extpl = {
    constructor: function(html) {
        var me = this,
            args = arguments,
            buffer = [],
            i = 0,
            length = args.length,
            value;

        me.initialConfig = {};
        if (length > 1) {
            for (; i < length; i++) {
                value = args[i];
                if (typeof value == 'object') {
                    fast.apply(me.initialConfig, value);
                    fast.apply(me, value);
                } else {
                    buffer.push(value);
                }
            }
            html = buffer.join('');
        } else {
            if (fast.isArray(html)) {
                buffer.push(html.join(''));
            } else {
                buffer.push(html);
            }
        }

        // @private
        me.html = buffer.join('');
        
        if (me.compiled) {
            me.compile();
        }
        
    },
    isTemplate: true,
    disableFormats: false,
    re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,
    ___apply: function(values) {
        var me = this,
            useFormat = me.disableFormats !== true,
            fm = fast.Format,
            tpl = me,
            ret;

        if (me.compiled) {
            return me.compiled(values).join('');
        }

        function fn(m, name, format, args) {
            if (format && useFormat) {
                if (args) {
                    args = [values[name]].concat(fast.functionFactory('return ['+ args +'];')());
                } else {
                    args = [values[name]];
                }
                if (format.substr(0, 5) == "this.") {
                    return tpl[format.substr(5)].apply(tpl, args);
                }
                else {
                    return fm[format].apply(fm, args);
                }
            }
            else {
                return values[name] !== undefined ? values[name] : "";
            }
        }

        ret = me.html.replace(me.re, fn);
        //ret = me.compile(ret);
        //console.log(ret);
        return ret;
    },

    /**
     * Appends the result of this template to the provided output array.
     * @param {Object/Array} values The template values. See {@link #apply}.
     * @param {Array} out The array to which output is pushed.
     * @return {Array} The given out array.
     */
    ___applyOut: function(values, out) {
        var me = this;

        if (me.compiled) {
            out.push.apply(out, me.compiled(values));
        } else {
            out.push(me.apply(values));
        }

        return out;
    },
    apply: function(values) {
        return this.applyOut(values, []).join('');      
    },
    
    applyOut: function(values, out) {
        var me = this;

        if (!me.fn) {
            me.fn = me.compile(me.html);
        }
        //console.log(me.fn);
        //console.log(values);
        out = me.fn(values);
        //這里玩的很精妙,以后有時間再分析一下
        //console.log(me.fn);
        //try {
        //    me.fn.call(me, out, values, {}, 1, 1);
        //} catch (e) {}
        //console.log(out);
        return out;
    },

    /**
     * @method applyTemplate
     * @member Ext.Template
     * Alias for {@link #apply}.
     * @inheritdoc Ext.Template#apply
     */
    applyTemplate: function () {
        return this.apply.apply(this, arguments);
    },

    /**
     * Sets the HTML used as the template and optionally compiles it.
     * @param {String} html
     * @param {Boolean} compile (optional) True to compile the template.
     * @return {Ext.Template} this
     */
    set: function(html, compile) {
        var me = this;
        me.html = html;
        me.compiled = null;
        return compile ? me.compile() : me;
    },

    compileARe: /\\/g,
    compileBRe: /(\r\n|\n)/g,
    compileCRe: /'/g,

    /**
     * Applies the supplied values to the template and inserts the new node(s) as the first child of el.
     *
     * @param {String/HTMLElement/Ext.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.Element} The new node or Element
     */
    insertFirst: function(el, values, returnElement) {
        return this.doInsert('afterBegin', el, values, returnElement);
    },

    /**
     * Applies the supplied values to the template and inserts the new node(s) before el.
     *
     * @param {String/HTMLElement/Ext.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.Element} The new node or Element
     */
    insertBefore: function(el, values, returnElement) {
        return this.doInsert('beforeBegin', el, values, returnElement);
    },

    /**
     * Applies the supplied values to the template and inserts the new node(s) after el.
     *
     * @param {String/HTMLElement/Ext.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.Element} The new node or Element
     */
    insertAfter: function(el, values, returnElement) {
        return this.doInsert('afterEnd', el, values, returnElement);
    },

    /**
     * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
     *
     * For example usage see {@link Ext.Template Ext.Template class docs}.
     *
     * @param {String/HTMLElement/Ext.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return an Ext.Element.
     * @return {HTMLElement/Ext.Element} The new node or Element
     */
    append: function(el, values, returnElement) {
        return this.doInsert('beforeEnd', el, values, returnElement);
    },

    doInsert: function(where, el, values, returnEl) {
        el = fast.id(el);
        //var newNode = Ext.DomHelper.insertHtml(where, el, this.apply(values));
        //return returnEl ? Ext.get(newNode, true) : newNode;
    },

    /**
     * Applies the supplied values to the template and overwrites the content of el with the new node(s).
     *
     * @param {String/HTMLElement/Ext.Element} el The context element
     * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
     * @param {Boolean} returnElement (optional) true to return a Ext.Element.
     * @return {HTMLElement/Ext.Element} The new node or Element
     */
    overwrite: function(el, values, returnElement) {
        el = fast.id(el);
        
        fast.html(el,this.apply(values));
        return el.firstChild;
    },
    ua : navigator.userAgent.toLowerCase(),
    ie : /msie(\d+\.\d+)/i.test(this.ua) ? (document.documentMode || (+RegExp['\x241'])) : undefined,
    useEval: /gecko/i.test(this.ua) && !/like gecko/i.test(this.ua),

    // See http://jsperf.com/nige-array-append for quickest way to append to an array of unknown length
    // (Due to arbitrary code execution inside a template, we cannot easily track the length in  var)
    // On IE6 and 7 myArray[myArray.length]='foo' is better. On other browsers myArray.push('foo') is better.
    useIndex: this.ie && this.ie < 8,

    useFormat: true,
    
    propNameRe: /^[\w\d\$]*$/,

    compile: function (tpl) {
        var me = this,tpl = tpl || me.html,
            code = me.generate(tpl);
            //console.log(tpl);
            //debugger;
            //console.log(code);
        return me.useEval ? me.evalTpl(code) : (new Function('window', code))(window);
    },

    generate: function (tpl) {
        var me = this;
        //console.log("me",me.fnArgs);
        me.body = [
            'var c0=values, a0 = fast.isArray(c0), p0=parent, n0=xcount || 1, i0=1, out=[], v;\n'
        ];
        me.funcs = [
            // note: Ext here is properly sandboxed
            'var fm=fast.Format;' 
        ];
        me.switches = [];

        me.parse(tpl);
        !me.fnArgs && (me.fnArgs = "values");
        me.funcs.push(
            (me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {',
                me.body.join(''),
            'return out;}'
        );

        var code = me.funcs.join('\n');

        return code;
    },

    //-----------------------------------
    // XTemplateParser callouts

    doText: function (text) {
        var me = this,
            out = me.body;

        text = text.replace(me.aposRe, "\\'").replace(me.newLineRe, '\\n');
        if (me.useIndex) {
            out.push('out[out.length]=\'', text, '\'\n');
        } else {
            out.push('out.push(\'', text, '\')\n');
        }
    },

    doExpr: function (expr) {
        var out = this.body;
        expr = expr.replace("values","vvv");
        out.push('if ((v=' + expr + ')!==undefined) out');
        if (this.useIndex) {
             out.push('[out.length]=String(v)\n');
        } else {
             out.push('.push(String(v))\n');
        }
    },

    doTag: function (tag) {
        this.doExpr(this.parseTag(tag));
    },

    doElse: function () {
        this.body.push('} else {\n');
    },

    doEval: function (text) {
        this.body.push(text, '\n');
    },

    doIf: function (action, actions) {
        var me = this;

        // If it's just a propName, use it directly in the if
        if (me.propNameRe.test(action)) {
            me.body.push('if (', me.parseTag(action), ') {\n');
        }
        // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
        else {
            me.body.push('if (', me.addFn(action), me.callFn, ') {\n');
        }
        if (actions.exec) {
            me.doExec(actions.exec);
        }
    },

    doElseIf: function (action, actions) {
        var me = this;

        // If it's just a propName, use it directly in the else if
        if (me.propNameRe.test(action)) {
            me.body.push('} else if (', me.parseTag(action), ') {\n');
        }
        // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
        else {
            me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n');
        }
        if (actions.exec) {
            me.doExec(actions.exec);
        }
    },

    doSwitch: function (action) {
        var me = this;

        // If it's just a propName, use it directly in the switch
        if (me.propNameRe.test(action)) {
            me.body.push('switch (', me.parseTag(action), ') {\n');
        }
        // Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
        else {
            me.body.push('switch (', me.addFn(action), me.callFn, ') {\n');
        }
        me.switches.push(0);
    },

    doCase: function (action) {
        var me = this,
            cases = Ext.isArray(action) ? action : [action],
            n = me.switches.length - 1,
            match, i;

        if (me.switches[n]) {
            me.body.push('break;\n');
        } else {
            me.switches[n]++;
        }

        for (i = 0, n = cases.length; i < n; ++i) {
            match = me.intRe.exec(cases[i]);
            cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe,"\\'") + "'");
        }

        me.body.push('case ', cases.join(': case '), ':\n');
    },

    doDefault: function () {
        var me = this,
            n = me.switches.length - 1;

        if (me.switches[n]) {
            me.body.push('break;\n');
        } else {
            me.switches[n]++;
        }

        me.body.push('default:\n');
    },

    doEnd: function (type, actions) {
        var me = this,
            L = me.level-1;

        if (type == 'for') {
            /*
            To exit a for loop we must restore the outer loop's context. The code looks
            like this (which goes with that produced by doFor:

                    for (...) { // the part generated by doFor
                        ...  // the body of the for loop

                        // ... any tpl for exec statement goes here...
                    }
                    parent = p1;
                    values = r2;
                    xcount = n1;
                    xindex = i1
            */
            if (actions.exec) {
                me.doExec(actions.exec);
            }

            me.body.push('}\n');
            me.body.push('parent=p',L,';values=r',L+1,';xcount=n',L,';xindex=i',L,'\n');
        } else if (type == 'if' || type == 'switch') {
            me.body.push('}\n');
        }
    },

    doFor: function (action, actions) {
        var me = this,
            s = me.addFn(action),
            L = me.level,
            up = L-1;
        me.body.push('var c',L,'=',s,me.callFn,', a',L,'=fast.isArray(c',L,'), p',L,'=c',up,',r',L,'=values\n',
            'parent=a',up,'?c',up,'[i',up,']:p',L,'\n',
            //'for (var i',L,'=0,n',L,'=a',L,'?c',L,'.length:(c',L,'?1:0), xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n',
            'for (var i0 = 0,i1=0, l0 = values.length,xcount=l0; i0 < l0; i0 += 1){\n',
            'vvv=values[i0]\n',
            'xindex=i',L,'+1\n');
    },

    doExec: function (action, actions) {
        var me = this,
            name = 'f' + me.funcs.length;

        me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
                            ' try { with(values) {',
                            '  ' + action,
                            ' }} catch(e) {}',
                      '}');

        me.body.push(name + me.callFn + '\n');
    },

    //-----------------------------------
    // Internal

    addFn: function (body) {
        var me = this,
            name = 'f' + me.funcs.length;
        !me.fnArgs && (me.fnArgs = "values");
        if (body === '.') {
            me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
                            ' return values',
                       '}');
        } else if (body === '..') {
            me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
                            ' return parent',
                       '}');
        } else {
            me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
                            ' try { with(values) {',
                            '  return(' + body + ')',
                            ' }} catch(e) {}',
                       '}');
        }

        return name;
    },

    parseTag: function (tag) {
        
        var m = this.tagRe.exec(tag),
            name = m[1],
            format = m[2],
            args = m[3],
            math = m[4],
            v;
        //console.log(m);
        // name = "." - Just use the values object.
        if (name == '.') {
            // filter to not include arrays/objects/nulls
            v = 'fast.inArray(["string", "number", "boolean"], typeof values) > -1 || fast.isDate(values) ? values : ""';
        }
        // name = "#" - Use the xindex
        else if (name == '#') {
            v = 'xindex';
        }
        else if (name.substr(0, 7) == "parent.") {
            v = name;
        }
        // compound Javascript property name (e.g., "foo.bar")
        else if (isNaN(name) && name.indexOf('-') == -1 && name.indexOf('.') != -1) {
            v = "values." + name;
        }
        // number or a '-' in it or a single word (maybe a keyword): use array notation
        // (http://jsperf.com/string-property-access/4)
        else {    
            v = "values['" + name + "']";
        }

        if (math) {
            v = '(' + v + math + ')';
        }
        //console.log(v);
        if (format && this.useFormat) {
            args = args ? ',' + args : "";
            if (format.substr(0, 5) != "this.") {
                format = "fm." + format + '(';
            } else {
                format += '(';
            }
        } else {
            return v;
        }
        
        return format + v + args + ')';
    },

    // @private
    evalTpl: function ($) {
        // We have to use eval to realize the code block and capture the inner func we also
        // don't want a deep scope chain. We only do this in Firefox and it is also unhappy
        // with eval containing a return statement, so instead we assign to "$" and return
        // that. Because we use "eval", we are automatically sandboxed properly.
        eval($);
        return $;
    },

    newLineRe: /\r\n|\r|\n/g,
    aposRe: /[']/g,
    intRe:  /^\s*(\d+)\s*$/,
    tagRe:  /([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?/,
    doTpl: function(){},

    parse: function (str) {
        
        //str = this.html;
        var me = this,
            len = str.length,
            aliases = { elseif: 'elif' },
            topRe = me.topRe,
            actionsRe = me.actionsRe,
            index, stack, s, m, t, prev, frame, subMatch, begin, end, actions;

        me.level = 0;
        me.stack = stack = [];

        for (index = 0; index < len; index = end) {
            topRe.lastIndex = index;
            m = topRe.exec(str);
            //console.log(m);
            if (!m) {
                me.doText(str.substring(index, len));
                break;
            }

            begin = m.index;
            end = topRe.lastIndex;

            if (index < begin) {
                me.doText(str.substring(index, begin));
            }

            if (m[1]) {
                end = str.indexOf('%}', begin+2);
                me.doEval(str.substring(begin+2, end));
                end += 2;
            } else if (m[2]) {
                end = str.indexOf(']}', begin+2);
                me.doExpr(str.substring(begin+2, end));
                end += 2;
            } else if (m[3]) { // if ('{' token)
                me.doTag(m[3]);
            } else if (m[4]) { // content of a <tpl xxxxxx> tag
                actions = null;
                while ((subMatch = actionsRe.exec(m[4])) !== null) {
                    s = subMatch[2] || subMatch[3];
                    if (s) {
                        s = fast.decodeHTML(s); // decode attr value
                        t = subMatch[1];
                        t = aliases[t] || t;
                        actions = actions || {};
                        prev = actions[t];

                        if (typeof prev == 'string') {
                            actions[t] = [prev, s];
                        } else if (prev) {
                            actions[t].push(s);
                        } else {
                            actions[t] = s;
                        }
                    }
                }

                if (!actions) {
                    if (me.elseRe.test(m[4])) {
                        me.doElse();
                    } else if (me.defaultRe.test(m[4])) {
                        me.doDefault();
                    } else {
                        me.doTpl();
                        stack.push({ type: 'tpl' });
                    }
                }
                else if (actions['if']) {
                    me.doIf(actions['if'], actions)
                    stack.push({ type: 'if' });
                }
                else if (actions['switch']) {
                    me.doSwitch(actions['switch'], actions)
                    stack.push({ type: 'switch' });
                }
                else if (actions['case']) {
                    me.doCase(actions['case'], actions);
                }
                else if (actions['elif']) {
                    me.doElseIf(actions['elif'], actions);
                }
                else if (actions['for']) {
                    ++me.level;
                    me.doFor(actions['for'], actions);
                    stack.push({ type: 'for', actions: actions });
                }
                else if (actions.exec) {
                    me.doExec(actions.exec, actions);
                    stack.push({ type: 'exec', actions: actions });
                }
                /*
                else {
                    // todo - error
                }
                /**/
            } else {
                frame = stack.pop();
                //console.log(frame);
                frame && me.doEnd(frame.type, frame.actions);
                if (frame && frame.type == 'for') {
                    --me.level;
                }
            }
        }
    },

    // Internal regexes
    
    topRe:     /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g,
    actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:["]([^"]*)["])|(?:[']([^']*)[']))\s*/g,
    defaultRe: /^\s*default\s*$/,
    elseRe:    /^\s*else\s*$/
};

調用方式:

<div id="exttpl" style="height:100px;overflow-y: auto"></div>
<script>
var etpl = ['<table width="100%" border=1>', '<tpl for=".">', '<tr>', 
'<td>{name}</td>', '<td>{sex}</td>', '<td>{age}</td>','<td>{date}</td>','<td>{uid}</td>', '</tr>', '</tpl>', '</table>'];
extpl.constructor(etpl);
extpl.overwrite("exttpl",data);
</script>

 

ext模版分兩種,一種是templete,數據模型只處理字符串和數組,另外一種是xtemplete,可以處理對象,上面剝離的是xtemplete。在剝離的過程不禁驚嘆js的精髓,原來js可以這樣寫!ext的大神們很精巧的拼裝了一個內置函數,核心函數在generateparse,generate負責組裝,parse負責解析。

然后測試了一下,速度更是驚人,幾乎和測試字符串模式(tp函數)跑平!那么多的判斷分支,神了,再膜拜一下ext。

細嚼完ext,於是又回頭看了一下jquery,由於時間問題沒來得及剝離,粗略的寫了一個用例。

<div style="height:100px;overflow-y: auto">
    <table width="100%" border=1 id="jqtpl"></table>
</div>
<script id='templateName' type='text/x-jquery-tmpl'>
<tr><td>${name}</td><td>${sex}</td><td>${age}</td><td>${date}</td><td>${uid}</td></tr>
</script>
<script type="text/javascript"> 
$('#templateName').tmpl(data).appendTo('#jqtpl');
</script> 

測試中,jquery的用時在最長的,因為沒有剝離出內核,所以不能妄加評論。但是它的寫法是最精簡的,值得學習和借鑒。

 

全部測試數據如下(單位:秒):

chrome:

測試對象的模式:用時 0.04700016975402832
測試字符串模式:用時 0.03299999237060547
測試extTmplete模式:用時 0.03299999237060547
測試jquerytmpl模式:用時 0.11500000953674316
 
ie9:
 
測試對象的模式:用時 0.44099998474121093
測試字符串模式:用時 0.03399991989135742
測試extTmplete模式:用時 0.032000064849853516
測試jquerytmpl模式:用時 0.3899998664855957
 
走了一圈之后再回顧自己寫的模版,發現了自己的很多不足,急於結果的實現,對過程的把控沒做合理的布局,實現上還需要做進一步推敲。
 
總結:
優秀js模版幾個關鍵因素:
 
一、支持多級數據,無論ext還是jquery都支持。比如data數據,模版內可以做data.param1循環也可以做data.param2循環。
二、支持模版助手helper,可以通過助手任意處理模版里的控件,給模版提供靈活性。
三、有完善的容錯機制。
四、支持內嵌循環。
五、易用性和速度效率,jquery的模版為什么會使用那么廣是有原因的,用戶能不能接受才是最關鍵的。
 
 

 

 

 

 

 

 

 


免責聲明!

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



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