zoukankan      html  css  js  c++  java
  • 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:"100%",border:"1"},child:tbody}],"example");

    当然,您也可以这样写:

    tpl([{tag:"table",attr:{style:"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的模版为什么会使用那么广是有原因的,用户能不能接受才是最关键的。
     
     

     

     

     

     

     

  • 相关阅读:
    《网络对抗》 后门原理与实践
    《网络对抗》 逆向及Bof进阶实践
    20145211黄志远《网络对抗》Exp9 Web安全基础实践
    20145211 《网络对抗》Exp8 Web基础
    20145211黄志远 《网络对抗》Exp7 网络欺诈技术防范
    20145211MSF基础应用实验
    20145211黄志远 《网络对抗技术》 恶意代码分析
    20145211黄志远 《网络对抗技术》 免杀原理与实践
    20145211黄志远 《网络对抗技术》 后门原理与实践
    20145211《网络对抗》注入Shellcode并执行&&Return-to-libc攻击
  • 原文地址:https://www.cnblogs.com/xpbb/p/2670618.html
Copyright © 2011-2022 走看看