zoukankan      html  css  js  c++  java
  • jQuery-template.js学习

    结构

    jQuery.each({..},function(){})
    jQuery.fn.extend({..})
    jQuery.extend({...})
    jQuery.extend(jQuery.tmpl,{..})
    function xx(){}//自定义方法

    结构上非常简单,但template插件却提供了不错的模版功能,我们根据API来慢慢看这个框架。

    网络资源http://www.cnblogs.com/FoundationSoft/archive/2010/05/19/1739257.html  http://www.jb51.net/article/27747.htm

    如果在原型上添加方法,这一般都是暴露给外部调用的API,我们来看一下,各个方法的流程:

    我们先看个例子:

    HTML结构:

    <table id="table1"></table>

    js部分:

    复制代码
    <script type="text/html" id="template1">
        <tr>
            <td>${ID}</td>
            <td>${Name}</td>
        </tr>
    </script>
    <script type="text/javascript" src="jquery-1.9.1.min.js"></script>
    <script type="text/javascript" src="jquery.tmpl.js"></script>
    <script type="text/javascript">
        var users = [
            {
                ID: 'think8848',
                Name: 'Joseph Chan',
                Langs: [
                    'Chinese',
                    'English'
                ]
            },
            {
                ID: 'aCloud',
                Name: 'Mary Cheung',
                Langs: [
                    'Chinese',
                    'French'
                ]
            }
        ];
    $('#template1').tmpl(users).appendTo('#table1')
    </script>
    复制代码

    可以看到模版被写在了type为text/html的script标签中,其中users是数据元,最后调用了一个$('#template1').tmpl(users)将信息写入模版,最后生成出的信息插入dom中,即完成。ok,来看一下jQuery原型上的tmpl方法

    tmpl: function( data, options, parentItem ) {
                return jQuery.tmpl( this[0], data, options, parentItem );//页面调用的时候的入口方法,这会去调用jQuery上的tmpl方法
            }

    进入jQuery上的tmpl方法

    复制代码
    tmpl: function( tmpl, data, options, parentItem ) {
                var ret, topLevel = !parentItem;
                if ( topLevel ) {
                    // This is a top-level tmpl call (not from a nested template using {{tmpl}})
                    parentItem = topTmplItem;//{ key: 0, data: {} }
                    tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );//根据参数数量,选择性的执行jQuery.template方法,这里获得了一个先有正则匹配,再经过拼接,最后new Function而得到一个匿名函数
                    wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
                } else if ( !tmpl ) {
                    // The template item is already associated with DOM - this is a refresh.
                    // Re-evaluate rendered template for the parentItem
                    tmpl = parentItem.tmpl;
                    newTmplItems[parentItem.key] = parentItem;
                    parentItem.nodes = [];
                    if ( parentItem.wrapped ) {
                        updateWrapped( parentItem, parentItem.wrapped );
                    }
                    // Rebuild, without creating a new template item
                    return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
                }
                if ( !tmpl ) {
                    return []; // Could throw...
                }
                if ( typeof data === "function" ) {//传进来的数据看是否存在函数
                    data = data.call( parentItem || {} );
                }
                if ( options && options.wrapped ) {
                    updateWrapped( options, options.wrapped );
                }
                ret = jQuery.isArray( data ) ?
                    jQuery.map( data, function( dataItem ) {
                        return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
                    }) :
                    [ newTmplItem( options, parentItem, tmpl, data ) ];
                //进入最后一层加工
                return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
            }
    复制代码

    对于这个例子,我们需要看一下这段代码的几个部分

    第一个部分:

    tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );//根据参数数量,选择性的执行jQuery.template方法,这里获得了一个先有正则匹配,再经过拼接,最后new Function而得到一个匿名函数

    tmpl参数则是那个写有模版的script对象,根据这个方法,我们进入jQuery.template方法。

    复制代码
    //这里经过几次进入template方法,最终还是将type为text/html的script对象传入template方法的第二个参数中
            template: function( name, tmpl ) {
                if (tmpl) {
                    // Compile template and associate with name
                    if ( typeof tmpl === "string" ) {//如何该参数是一个字符串,这里支持将模版以字符串形式写入
                        // This is an HTML string being passed directly in.
                        tmpl = buildTmplFn( tmpl );
                    } else if ( tmpl instanceof jQuery ) {
                        tmpl = tmpl[0] || {};//获取dom对象否则赋空对象
                    }
                    if ( tmpl.nodeType ) {//如何该参数是一个dom节点// If this is a template block, use cached copy, or generate tmpl function and cache.
                        tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));//根据正则生成一个匿名函数返回// Issue: In IE, if the container element is not a script block, the innerHTML will remove quotes from attribute values whenever the value does not include white space.
                        // This means that foo="${x}" will not work if the value of x includes white space: foo="${x}" -> foo=value of x.
                        // To correct this, include space in tag: foo="${ x }" -> foo="value of x"
                    }
                    return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;//jQuery.template方法返回了这个匿名函数,将匿名函数分装在jQuery.template[name]中便于以后调用
                }
                // Return named compiled template
                return name ? (typeof name !== "string" ? jQuery.template( null, name ):
                    (jQuery.template[name] ||
                        // If not in map, and not containing at least on HTML tag, treat as a selector.
                        // (If integrated with core, use quickExpr.exec)
                        jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;
            }
    复制代码

    这段代码中的一些逻辑判断,会在后面的API描述中介绍,我们先看到一个很重要的自定义方法buildTmplFn,这算是这个插件比较重要的一个部分。传入参数则是模版字符串

    buildTmplFn:

    复制代码
    function buildTmplFn( markup ) {
            //注意这里在return之前,会将Function构造器里的字符串生成匿名函数,注意这里的写法
            return new Function("jQuery","$item",
                // Use the variable __ to hold a string array while building the compiled template. (See https://github.com/jquery/jquery-tmpl/issues#issue/10).
                "var $=jQuery,call,__=[],$data=$item.data;" +
    
                // Introduce the data as local variables using with(){}
                "with($data){__.push('" +
    
                // Convert the template into pure JavaScript
                jQuery.trim(markup)
                    .replace( /([\'])/g, "\$1" )//将或者'前面都添加一个转义符
                    .replace( /[
    	
    ]/g, " " )//将空格符全部转成空字符串
                    .replace( /${([^}]*)}/g, "{{= $1}}" )//将类似${name}这种写法的转成{{=name}},换句话说,在页面script中也可以使用${name}来赋值,这里都会统一转成{{=name}}格式
                    .replace( /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g,
                    //replace的自定义方法中的参数个数表明正则所匹配的分组成员的个数,一般第一个参数是匹配的整个字符串,也就是说,上面的这条正则分组成员应该是6个
                    function( all, slash, type, fnargs, target, parens, args ) {
                        /*
                        * type表示你具体需要显示的文本功能,我们这个例子是=,表示仅仅是显示
                        * */
                         var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
                        if ( !tag ) {//如何插件中不存在相应配置,抛出异常
                            throw "Unknown template tag: " + type;
                        }
                        def = tag._default || [];
                        if ( parens && !/w$/.test(target)) {
                            target += parens;//拼接主干信息
                            parens = "";
                        }
                        //从正则的匹配来看,这个target是我们匹配获得的主要成员
                        if ( target ) {
                            target = unescape( target );//去转义符
                            args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
                            // Support for target being things like a.toLowerCase();
                            // In that case don't call with template item as 'this' pointer. Just evaluate...
                            //以下两种方法主要拼接字符串,最后转成函数执行。
                            expr = parens ? (target.indexOf(".") > -1 ? target + unescape( parens ) : ("(" + target + ").call($item" + args)) : target;
                            exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
                        } else {
                            exprAutoFnDetect = expr = def.$1 || "null";
                        }
                        fnargs = unescape( fnargs );//去转义符
                        //return的时候,再进行一次拼接,这里源码采用占位符的方式,先split再join的方式实现替换,大家也可以尝试使用正则替换。比较比较执行效率
                        return "');" +
                            tag[ slash ? "close" : "open" ]
                                .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )//这种方法可以学习一下,先使用占位符站住你需要替换的信息,然后使用split分隔开成数组,再使用join方法加入参数合成字符串,在数组中join的效率还是不错的
                                .split( "$1a" ).join( exprAutoFnDetect )//将之前拼接好的字符串替换占位符$1a
                                .split( "$1" ).join( expr )//替换$1
                                .split( "$2" ).join( fnargs || def.$2 || "" ) +//依旧是替换
                            "__.push('";
                    }) +
                "');}return __;"
            );
        }
    复制代码

    其实这个方法的作用就是根据内置正则表达式,解析模版字符串,截取相应的数据,拼凑成一个以后使用的匿名函数。这个匿名函数的功能主要将我们之后传入的数据源users根据正则解析,加入到模版字符串中。既然正则是这个方法的核心,那我们就来看一下这些正则,前几个正则比较简单,最后一个正则比较复杂,我们将它做拆解来理解。

    复制代码
    /*
        *           {{                                 --匹配{{
        *           (/?)                                --优先匹配/,捕捉匹配结果                                 ($1)slash
        *           (w+|.)                              --优先匹配字符,捕获匹配结果                              ($2)type
        *           (?:                                  --匹配但不捕获
        *               (                               --匹配(
        *               (                                --捕获匹配结果                                           ($3)fnargs
        *                   (?:                          --匹配但不捕捉
        *                       [^}]|}                 --优先匹配非},如果有},要求匹配这个}后面不能再出现}
        *                       (?!})                   --否定顺序环视,不能存在}
        *                   )*?                          --非优先匹配设定,尽可能少的去匹配
        *               )?                               --优先匹配
        *               )                               --匹配)
        *           )?                                   --优先匹配
        *           (?:                                  --匹配但不捕捉
        *               s+                              --优先匹配,匹配空格符,至少一个
        *               (.*?)?                           --非优先设定,尽可能少的去匹配,但必须要尽量尝试。         ($4)target
        *           )?                                   --优先匹配
        *           (                                    --捕获匹配结果                                          ($5)parens
        *               (                               --匹配(
        *               (                                --捕获匹配结果                                          ($6)args
        *                   (?:                          --匹配但不捕获
        *                       [^}]|}                 --优先匹配非},如果有},要求匹配这个}后面不能再出现}
        *                       (?!})                   --否定顺序环视,不能存在}
        *                   )*?                          --非优先匹配设定,尽可能少的去匹配
        *               )
        *               )                               --匹配)
        *            )?                                  --优先匹配
        *            s*                                 --优先匹配,空白符
        *            }}                                --匹配}}
        *            /g                                  --全局匹配
        *
        *
    复制代码

    因为replace的解析函数中一共有7个参数,除了第一个参数表示全部匹配外,其他都是分组内的匹配。我在注释中都一一列出,方便我们阅读。观察一下正则,我们可以了解这个插件给与我们的一些语法使用,比如说:

    页面模版内可以这样写:

    ${name}
    {{= name}}

    这两种写法都是对的,为什么前一条正则就是将${name}转成{{= name}},另外为什么=与name之间需要有空格呢?其实答案在正则里,看一下($4)target匹配的前一段是s+,这表明必须至少要匹配一个空格符。先将我们缩写的格式转成{{= xx}}再根据(.*?)?查找出xx的内容,也就是name,其实正则的匹配过程并不是像我所说的这样,在js中的正则在量词的出现时,会进行优先匹配,然后再慢慢回溯,我这样只是形象的简单说一下。对于这条正则,我们在后续的API中继续延伸。

    对于另外一个让我们学习的地方,那就是使用占位符插入我们所要的信息,一般我们都会使用正则,本插件也提供了一种不错的思路。先使用占位符,然后通过split(占位符)来分隔字符串,最后使用join(信息)来再次拼接字符串。这两个方法都是原生的,效率的话,我不太确定,应该还不错,有兴趣的朋友可以写写正则,在不同浏览器下比比看,谁的效率更高一点。

    既然它生成了一个匿名函数,我们可以简单地打印一下看看:

    复制代码
    function anonymous(jQuery, $item) {
       var $=jQuery,call,__=[],$data=$item.data;
       with($data){__.push('<tr>         <td>');
       if(typeof(ID)!=='undefined' && (ID)!=null){
          __.push($.encode((typeof(ID)==='function'?(ID).call($item):(ID))));
       }
       __.push('</td>         <td>');
       if(typeof(Name)!=='undefined' && (Name)!=null){
           __.push($.encode((typeof(Name)==='function'?(Name).call($item):(Name))));
    }
        __.push('</td>     </tr>');}return __;
    }
    复制代码

    这里with有延长作用域的作用,在一般的开发中,不建议使用,不太易于维护,那这个with括号里的ID,Name其实都是$data.ID和$data.Name,在没有调用这个匿名函数之前,我们先简单看一下,传入的$item参数拥有data属性,如果这个data的ID和Name不是函数的话就正常显示,如果是函数的话,则这些方法需要通过$item来调用。另外匿名函数中也拥有了这钱我们所写的模版结构,后续的工作就是用真实的数据去替换占位符,前提非空。ok,回到jQuery的tmpl方法中,我们再看一个比较重要的部分。

    ret = jQuery.isArray( data ) ?
                    jQuery.map( data, function( dataItem ) {
                        return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
                    }) :
                    [ newTmplItem( options, parentItem, tmpl, data ) ];

    data是用户传入的信息元,就是users,是一个数组,调用jQuery.map来进行遍历,来调用newTmplItem方法,其中tmpl则是刚才我们生成的匿名函数。

    复制代码
    function newTmplItem( options, parentItem, fn, data ) {
            // Returns a template item data structure for a new rendered instance of a template (a 'template item').
            // The content field is a hierarchical array of strings and nested items (to be
            // removed and replaced by nodes field of dom elements, once inserted in DOM).
    
            var newItem = {
                data: data || (data === 0 || data === false) ? data : (parentItem ? parentItem.data : {}),
                _wrap: parentItem ? parentItem._wrap : null,
                tmpl: null,
                parent: parentItem || null,
                nodes: [],
                calls: tiCalls,
                nest: tiNest,
                wrap: tiWrap,
                html: tiHtml,
                update: tiUpdate
            };
            if ( options ) {
                jQuery.extend( newItem, options, { nodes: [], parent: parentItem });
            }
            if ( fn ) {
                // Build the hierarchical content to be used during insertion into DOM
                newItem.tmpl = fn;
                newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
                newItem.key = ++itemKey;//表示计数
                // Keep track of new template item, until it is stored as jQuery Data on DOM element
                (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;//这里考虑一个页面可能多处使用模版,这里进行的编号,封装。
            }
            return newItem;//最后返回这个newItem对象
        }
    复制代码

    如果看到newItem的定义方式,或许之前我们对匿名函数的猜测有了一些佐证,没错,最后通过newItem.tmpl(jQuery,newItem)来调用了这个匿名函数,这个方法除了调用执行了匿名函数,还简单的封装了一下,便于以后我们调用$.tmplItem来获取相应的数据元信息。

     将生成好的ret传入最后一个加工方法build,完成整个模版的赋值

    复制代码
    //将函数等细化出来,拼接成字符串
        function build( tmplItem, nested, content ) {
            // Convert hierarchical content into flat string array
            // and finally return array of fragments ready for DOM insertion
            var frag, ret = content ? jQuery.map( content, function( item ) {
                //给所有标签加上_tmplitem=key的属性,也就是这条正则的含义
                return (typeof item === "string") ?
                    // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
                    (tmplItem.key ? item.replace( /(<w+)(?=[s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "="" + tmplItem.key + "" $2" ) : item) :
                    // This is a child template item. Build nested template.
                    build( item, tmplItem, item._ctnt );
            }) :
            // If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
            tmplItem;
            if ( nested ) {
                return ret;
            }
            // top-level template
            ret = ret.join("");//生成最终的模版
            // Support templates which have initial or final text nodes, or consist only of text
            // Also support HTML entities within the HTML markup.
            //这条正则比较简单,我们来看过一下。获得<>内的主要信息
            ret.replace( /^s*([^<s][^<]*)?(<[wW]+>)([^>]*[^>s])?s*$/, function( all, before, middle, after) {
                frag = jQuery( middle ).get();//将生成的jQuery dom对象转成数组集合,集合的每个成员则是对应生成的jQuery对象的原生dom对象
                //解析生成出来的dom
                storeTmplItems( frag );
                if ( before ) {
                    frag = unencode( before ).concat(frag);
                }
                if ( after ) {
                    frag = frag.concat(unencode( after ));
                }
            });
            return frag ? frag : unencode( ret );
        }
    复制代码

     这个里面出现了两条正则,我们分别看一下:

    复制代码
    *
        *       /
        *       (                                        --匹配捕获($1)
        *           <w+                                 --匹配<,字母或数字或下划线或汉字(至少一个,优先匹配)(存在固化分组的含义)
        *       )
        *       (?=[s>])                                --顺序环视,后面必须有空格和一个>
        *       (?![^>]*_tmplitem)                       --顺序否定环视,后面不能有非>字符,还有_tmplitem这些字符串
        *       (                                        --匹配捕获($2)
        *           [^>]*                                --匹配非>字符,优先匹配,任意多个
        *       )
        *       /g                                       --全局匹配
        *
    复制代码
    复制代码
    *
        *       ^                                        --开始
        *       s*                                      --优先匹配,任意多空白符
        *       (                                        --匹配捕获                                 ($1)before
        *           [^<s]                               --匹配非<或者是空白符
        *           [^<]*                                --优先匹配,匹配非<
        *       )?                                       --优先匹配
        *       (                                        --匹配捕获                                 ($2)middle
        *           <[wW]+>                            --匹配<,任意字符(至少一个,优先匹配),>
        *       )
        *       (                                        --匹配捕获                                 ($3)after
        *           [^>]*                                --匹配非>
        *           [^>s]                               --匹配非>或者是空白符
        *       )?                                       --优先匹配(0,1次)
        *       s*                                      --匹配空白符(任意次,优先匹配)
        *       $                                        --结束
        *
        *
    复制代码

    前一个正则的作用是给标签加上_tmplitem=key的属性,后一条正则则是获得<>内的主要信息。最后进入storeTmplItems方法

    复制代码
    function storeTmplItems( content ) {
            var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
            for ( i = 0, l = content.length; i < l; i++ ) {
                if ( (elem = content[i]).nodeType !== 1 ) {//如果该节点不是元素节点,则直接跳过
                    continue;
                }
                //这里将会找到关键的几个元素节点,在模版中可能会存在注释节点,文本节点。
                //遍历元素节点
                elems = elem.getElementsByTagName("*");
                for ( m = elems.length - 1; m >= 0; m-- ) {//自减的遍历有时候比自增要好很多
                    processItemKey( elems[m] );
                }
                processItemKey( elem );
            }
    复制代码

    作为储存节点的方法,使用processItemKey进行遍历。

    复制代码
    function processItemKey( el ) {
                var pntKey, pntNode = el, pntItem, tmplItem, key;
                // Ensure that each rendered template inserted into the DOM has its own template item,
                //确保每个呈现模板插入到DOM项目有自己的模板
                if ( (key = el.getAttribute( tmplItmAtt ))) {//查看这个元素上是否有_tmplitem这个属性,限定了属于某个模版的内容
                    while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }//这种写法也比较不错,使用while不停向上查询pntNode的父节点
                    if ( pntKey !== key ) {//父节点存在,但是没有_tmplitem这个属性,一般是文档碎片
                        // The next ancestor with a _tmplitem expando is on a different key than this one.
                        // So this is a top-level element within this template item
                        // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
                        //如果该元素的父节点不存在,则可能是文档碎片
                        pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
                        if ( !(tmplItem = newTmplItems[key]) ) {
                            // The item is for wrapped content, and was copied from the temporary parent wrappedItem.
                            tmplItem = wrappedItems[key];
                            tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode] );
                            tmplItem.key = ++itemKey;
                            newTmplItems[itemKey] = tmplItem;
                        }
    
                        if ( cloneIndex ) {
                            cloneTmplItem( key );
                        }
                    }
                    el.removeAttribute( tmplItmAtt );//最后去除_tmplitem这个属性
                } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
                    //这是一个元素,呈现克隆在附加或appendTo等等
                    //TmplItem存储在jQuery cloneCopyEvent数据已经被克隆。我们必须换上新鲜的克隆tmplItem。
                    // This was a rendered element, cloned during append or appendTo etc.
                    // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
                    cloneTmplItem( tmplItem.key );
                    newTmplItems[tmplItem.key] = tmplItem;
                    pntNode = jQuery.data( el.parentNode, "tmplItem" );
                    pntNode = pntNode ? pntNode.key : 0;
                }
                if ( tmplItem ) {//遍历到最外层的元素
                    pntItem = tmplItem;
                    //找到父元素的模板项。
                    // Find the template item of the parent element.
                    // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
                    while ( pntItem && pntItem.key != pntNode ) {//顶级为pntNode为0
                        // Add this element as a top-level node for this rendered template item, as well as for any
                        // ancestor items between this item and the item of its parent element
                        pntItem.nodes.push( el );
                        pntItem = pntItem.parent;//向上迭代
                    }
                    // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
                    delete tmplItem._ctnt;//删除属性
                    delete tmplItem._wrap;//删除属性
                    // Store template item as jQuery data on the element
                    jQuery.data( el, "tmplItem", tmplItem );//这样可以$(el).data('tmplItem')读取tmplItem的值
                }
                function cloneTmplItem( key ) {
                    key = key + keySuffix;
                    tmplItem = newClonedItems[key] =
                        (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent ));
                }
            }
    复制代码

    根据之前添加的_tmplitem属性,做了完整的向上遍历查找,最后删除掉_tmplitem属性。build方法将frag参数uncode之后返回给jQuery.tmpl方法来返回,最后通过appendTo加入到dom中,生成我们所看到的结果。以上通过一个简单的例子粗略的过了一下插件的运行流程,我们来看一些官方的API。

    1.$.template,将HTML编译成模版

     例子1

    var markup = '<tr><td>${ID}</td><td>${Name}</td></tr>'; 
    $.template('template', markup); 
    $.tmpl('template', users).appendTo('#templateRows'); 

    直接看一下$.template方法

    if ( typeof tmpl === "string" ) {//如何该参数是一个字符串,这里支持将模版以字符串形式写入
                        // This is an HTML string being passed directly in.
                        tmpl = buildTmplFn( tmpl );
                    }

    可以看到,我们传入的markup是一个字符串,直接将这个markup传入buildTmplFn中去生成一个匿名函数。

    return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;//jQuery.template方法返回了这个匿名函数,将匿名函数分装在jQuery.template[name]中便于以后调用

    插件内部将编译好的HTML模版的匿名函数存入了jQuery.template[name]中,便于我们以后调用。

    tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );//根据参数数量,选择性的执行jQuery.template方法,这里获得了一个先有正则匹配,再经过拼接,最后new Function而得到一个匿名函数

    这里插件先查找了jQuery.template看是否存在tmpl的已经生成好的匿名函数,有则直接使用,否则重新生成。获得了匿名函数,其他步骤跟之前一样。

    2.jQuery.tmpl()有两个比较有用的参数$item,$data,其中$item表示当前模版,$data表示当前数据

    例子2

    复制代码
    <script type="text/html" id="template1">
        <tr>
            <td>${ID}</td>
            <td>${$data.Name}</td>
            <td>${$item.getLangs(';')}</td>
        </tr>
    </script>
    
    var users = [
            {
                ID: 'think8848',
                Name: 'Joseph Chan',
                Langs: [
                    'Chinese',
                    'English'
                ]
            },
            {
                ID: 'aCloud',
                Name: 'Mary Cheung',
                Langs: [
                    'Chinese',
                    'French'
                ]
            }
        ]
        $('#template1').tmpl(users,{
            getLangs: function(separator){
                return this.data.Langs.join(separator);
            }
        }).appendTo('#table1');
    复制代码
    <table id="table1"></table>

    乍一看,调用的方式是一样的,你会疑问为什么模版里要用$item和$data这样的形式,其实你仔细看一下上个例子生成的匿名函数,就能发现这里这么写其实是为了更好的拼接。以下是这个例子所生成的匿名函数:

    function anonymous(jQuery, $item) {
    var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('<tr>         <td>');if(typeof(ID)!=='undefined' && (ID)!=null){__.push($.encode((typeof(ID)==='function'?(ID).call($item):(ID))));}__.push('</td>         <td>');if(typeof($data.Name)!=='undefined' && ($data.Name)!=null){__.push($.encode((typeof($data.Name)==='function'?($data.Name).call($item):($data.Name))));}__.push('</td>         <td>');if(typeof($item.getLangs)!=='undefined' && ($item.getLangs)!=null){__.push($.encode($item.getLangs(';')));}__.push('</td>     </tr>');}return __;

    $data是$item的一个属性,存储着数据,$item中同样有很多自定义方法。这里getLangs方法里的this在匿名函数具体调用的时候会指向$item,这里需要注意一下。在newTmplItem方法里执行我们生成的匿名函数,这里都没有什么问题,这里我们通过正则简单回看一下这个${ID},${$data.Name}是如何匹配的。这两个匹配其实是一个道理,匹配的正则如下:

    /{{(/?)(w+|.)(?:(((?:[^}]|}(?!}))*?)?))?(?:s+(.*?)?)?((((?:[^}]|}(?!}))*?)))?s*}}/g

    大家对照我之前的分解表看比较方便。我们拿${$data.Name}举例,不过使用之前,它已经转成{{= $data.Name}}

    1:匹配{{

    2:尝试匹配(/?),?表示优先匹配,但是{{后面没有/,所以匹配无效,?表示最多成功匹配一个。继续后面的匹配

    3:尝试匹配(w+|.),如果|左边的能匹配成功则不需要进行右边的匹配,所以w+会尽可能去匹配,但是w无法匹配=所以,尝试用|右边的.去匹配,.可以匹配=,因为没有量词,所以只能匹配这个=

    4:尝试匹配(?:(((?:[^}]|}(?!}))*?)?))?

      4.1:(?:)表示匹配但不捕获,其里面第一个要匹配的是(,可以看到{{=后面是空格而不是(所以匹配失败,加上这不捕获的分组使用的是优先量词?,允许匹配为空,继续后面的匹配

    5:尝试匹配(?:s+(.*?)?)?

      5.1:分组里第一个匹配s+,匹配=后面的空格符号,继续尝试匹配,当匹配到$时发现无法匹配,则s+匹配结束。

      5.2:尝试匹配(.*?)?,分组外围使用的是?,尽可能尝试匹配一个看看,对于(.*?)匹配$,因为(.*?)是惰性匹配(不优先匹配),所以系统选择不匹配,另外外围的?也允许匹配不成功。继续后面的匹配

    6:尝试匹配((((?:[^}]|}(?!}))*?)))?

      6.1:如果4的步骤不匹配,那5中的(同样无法匹配$,所以匹配失败

    7:尝试匹配s*}},如果从$开始匹配,果断匹配失败。整个匹配结束了么?其实还没有,开始对惰性匹配继续进行匹配

    8:让(.*?)先匹配$,再执行5,6步骤,如果最终匹配失败了,继续让(.*?)匹配$d,依次类推,直到(.*?)匹配到$data.Name,这时6结果匹配成功。整个正则匹配匹配成功。

    以上则是该正则的一次简单匹配过程,可以发现该正则使用了惰性匹配一定程度上减少了正则的回溯次数,提高了效率。

    3.each的用法

    例子:

    复制代码
    <script type="text/html" id="template1">
        <li>
            ID: ${ID}; Name: ${Name};
            <br />Langs:
            <ul>
                <STRONG>
                    {{each(i,lang) Langs}}
                    <li>${i + 1}:
                        <label>${lang}. </label>
                    </li>
                    {{/each}}
                </STRONG>
            </ul>
        </li>
    
    </script>
    var users = [
            {
                ID: 'think8848',
                Name: 'Joseph Chan',
                Langs: [
                    'Chinese',
                    'English'
                ]
            },
            {
                ID: 'aCloud',
                Name: 'Mary Cheung',
                Langs: [
                    'Chinese',
                    'French'
                ]
            }
        ];
        $('#template1').tmpl(users).appendTo('#eachList')
    复制代码
    <ul id="eachList"></ul>

    运行过程基本一致,我们就看两个部分:

    3.1:正则匹配

    3.2:如何实现each

    之前的${ID},${Name}和之前的匹配是一致的,这里就不描述了,看一下这段字符串的匹配。

    {{each(i,lang) Langs}}
                    <li>${i + 1}:
                        <label>${lang}. </label>
                    </li>
                    {{/each}}

    主要是{{each(i,lang) Langs}}和{{/each}}这两条的匹配

    {{each(i,lang) Langs}}

    1:匹配{{

    2:尝试匹配(/?),/不能与e相匹配,所以匹配失败,因为存在?量词,继续下面的匹配

    3:尝试匹配(w+|.),其中W+是优先匹配,所以它一直匹配到each,当它尝试匹配(时,发现匹配失败时,则就返回匹配结果each进入分组,继续下面的匹配

    4:尝试匹配(?:(((?:[^}]|}(?!}))*?)?))?

      4.1:首先匹配(

      4.2:尝试匹配((?:[^}]|}(?!}))*?)?

        4.2.1:尝试匹配(?:[^}]|}(?!}))*?,这里实际就是两个部分[^}]|}和(?!}),这里的正则写的有点复杂,其实也不难理解。这两个匹配他使用(?:)*?表示匹配后不捕捉,并且是惰性匹配,而却在它的外层加了()?,表示捕获分组,可想而                        知是为了能更多的捕捉到全部的全部条件的字符串,因为里层的是惰性匹配,所以系统默认不匹配,继续后面的匹配

    5:尝试匹配(?:s+(.*?)?)?,发现i无法与s+匹配,匹配失败,返回到惰性匹配那。

    6:尝试让惰性匹配(?:[^}]|}(?!}))*?去匹配字符串,我们先看一下[^}]|}(?!}),这样看,以|为分割点,左边是[^}],右边是}(?!}),这就清楚了,可以匹配非}的字符,如果匹配失败,就匹配},但是它的后面不能再有},所以系统先使用[^}]去匹配i,再去执行5,如果5仍不能满足,则继续匹配i,直到5匹配满足,而此时系统已经匹配到了(i,lang)

    7:(?:s+(.*?)?)?中的(.*?)?依旧是惰性匹配,系统先尝试不匹配

    8:尝试匹配(?:(((?:[^}]|}(?!}))*?)?))?,发现匹配失败,因为量词的缘故,继续后续的匹配

    9:尝试匹配s*}},如果从$开始匹配,果断匹配失败。

    10:返回到惰性匹配那,让(.*?)尝试匹配L,再执行8,9步,直到它能满足,如果不能正则匹配不成功。最后(.*?)匹配了Langs,完成了整个正则的匹配。

     那{{/each}}则就是一个道理。但要注意这个/,因为如果/匹配了,那replace匹配函数中的slash将会是/,则根据tag[ slash ? "close" : "open" ],它将使用tag['close']来闭合这个each,这也就是为什么拥有open的close的原因。

    关于each是如何实现的,我们需要看到源码的这个部分:

    "each": {
                    _default: { $2: "$index, $value" },
                    open: "if($notnull_1){$.each($1a,function($2){with(this){",
                    close: "}});}"
                }

    replace的匹配方法中有7个参数,其中type参数就是each,根据

    var tag = jQuery.tmpl.tag[ type ]

    这里我们可以看到其实实现each的功能仅仅是将$.each写入字符串中,它的参数有$index和$value,这其实就是jQuery的each方法。代码的后续会将其取出,进行拼接。

    4.if和else的用法

    例子:

    复制代码
    <script type="text/html" id="template1">
        <tr>
            <td>${ID}</td>
            <td>${Name}</td>
            <td>
                {{if Langs.length > 1}}
                    ${Langs.join('; ')}
                {{else}}
                    ${Langs}
                {{/if}}
            </td>
        </tr>
    </script>
        var users = [
            {
                ID: 'think8848',
                Name: 'Joseph Chan',
                Langs: [
                    'Chinese',
                    'English'
                ]
            },
            {
                ID: 'aCloud',
                Name: 'Mary Cheung',
                Langs: [
                    'Chinese',
                    'French'
                ]
            }
        ]
    $('#template1').tmpl(users).appendTo('#table1');
    复制代码
    <table id="table1"></table>

    其实if,else跟each差不多在正则匹配的时候,这里我就不重复了。看一下对应的函数

    复制代码
    "if": {
                    open: "if(($notnull_1) && $1a){",
                    close: "}"
                },
                "else": {
                    _default: { $1: "true" },
                    open: "}else if(($notnull_1) && $1a){"
                },
    复制代码

     注意一下,在这里if拥有close而else则没有,反映到模版书写上,闭合的时候我们只需要写{{/if}}就可以了,不需要写{{/else}}

    5.html占位符

    例子5:

    复制代码
    <script type="text/html" id="template1">
        <tr>
            <td>${ID}</td>
            <td>${Name}</td>
            <td>{{html Ctrl}}</td>
        </tr>
    </script>
    
    var users = [
        {
            ID: 'think8848',
            Name: 'Joseph Chan',
            Ctrl: '<input type="button" value="Demo"/>'
        },
        {
            ID: 'aCloud',
            Name: 'Mary Cheung',
            Ctrl: '<input type="button" value="Demo"/>'
        }
    ];
        $('#template1').tmpl(users).appendTo('#table1')
        $('table').delegate('tr','click',function(){
            var item = $.tmplItem(this);
            alert(item.data.Name);
        })
    复制代码
    <table id="table1"></table>

    这里看一下模版的{{html Ctrl}},匹配规则还是一样的。看一下拓展的部分:

    "html": {
                    // Unecoded expression evaluation.
                    open: "if($notnull_1){__.push($1a);}"
                }

    注意,这时允许你脚本插入的,也就是如果你插入一个<script type="text/javascript" >alert(1)</script>,生成的页面是可以弹出alert(1)的。这跟跟换ID和Name是一个意思。

    6.{{tmpl}}

     例子6:

    复制代码
    <script type="text/html" id="template1">
        <tr>
            <td>${ID}</td>
            <td>${Name}</td>
            <td>{{tmpl($data) '#template2'}}</td>
        </tr>
    </script>
    <script type="text/html" id="template2">
        {{each Langs}}
            ${$value}
        {{/each}}
    </script>
    var users = [
            {
                ID: 'think8848',
                Name: 'Joseph Chan',
                Langs:[
                    'Chinese',
                    'English'
                ]
            },
            {
                ID: 'aCloud',
                Name: 'Mary Cheung',
                Langs: [
                    'Chinese',
                    'French'
                ]
            }
        ];
        $('#template1').tmpl(users).appendTo('#table1');
    复制代码
    <table id="table1"></table>

    看一下{{tmpl($data) '#template2'}},正则匹配是跟以前一样的。我们看一下扩展

    复制代码
    "tmpl": {
                    _default: { $2: "null" },
                    open: "if($notnull_1){__=__.concat($item.nest($1,$2));}"
                    // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
                    // This means that {{tmpl foo}} treats foo as a template (which IS a function).
                    // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
                }
    复制代码

    注意里面有个方法nest,找到newTmplItem方法里的我们定义的newItem,看一下,它里面是否有个属性是nest,有,是tiNest,看一下tiNest

    function tiNest( tmpl, data, options ) {
            // nested template, using {{tmpl}} tag
            return jQuery.tmpl( jQuery.template( tmpl ), data, options, this );
        }

    这里我们大概可以了解这种解析过程,先template1的模版,我们在template1中标记了tmpl,当我们第一次执行匿名函数的时候,它执行nest方法,再次去执行jQuery.tmpl,然后你们懂的,生成关于template2的匿名函数等等。所以这里模版的1中的指向id千万不要写错,否则报错。

    看到jQuery.template方法中的这个部分

    return name ? (typeof name !== "string" ? jQuery.template( null, name ):
                    (jQuery.template[name] ||
                        // If not in map, and not containing at least on HTML tag, treat as a selector.
                        // (If integrated with core, use quickExpr.exec)
                        jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;

    因为我们第一次没有存储匿名函数(保存模板的作用),也不需要存储。所以执行jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )),这里我们看到一条正则

    htmlExpr = /^[^<]*(<[wW]+>)[^>]*$|{{! /

    这个比较简单,留给读者吧,呵呵。匹配结果当然是不满足,我们使用jQuery()去创建jQuery对象,重新执行template方法,生成相应的匿名函数等。

    7.{{wrap}}包装器

    例子:

    复制代码
    <script type="text/html" id="myTmpl">
        The following wraps and reorder some HTML content:
        {{wrap "#tableWrapper"}}
        <h3>One</h3>
        <div>
            First: <b>content</b>
        </div>
        <h3>Two</h3>
        <div>
            And <em>more</em> <b>content</b>
        </div>
        {{/wrap}}
    </script>
    <script type="text/html" id="tableWrapper">
        <table cellspacing="0" cellpadding="3" border="1">
            <tbody>
                <tr>
                    {{each $item.html("h3",true)}}
                    <td>
                        ${$value}
                    </td>
                    {{/each}}
                </tr>
                <tr>
                    {{each $item.html("div")}}
                    <td>
                        {{html $value}}
                    </td>
                    {{/each}}
                </tr>
            </tbody>
        </table>
    </script>
    复制代码
    <div id="wrapDemo"></div>

    依照惯例,看一下拓展部分

    "wrap": {
                    _default: { $2: "null" },
                    open: "$item.calls(__,$1,$2);__=[];",
                    close: "call=$item.calls();__=call._.concat($item.wrap(call,__));"
                }

    这里我们看到了两个新方法:calls()和wrap(),找到newTmplItem里面的newItem,来看一下这两个方法

     calls:

    function tiCalls( content, tmpl, data, options ) {
            if ( !content ) {
                return stack.pop();
            }
            stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options });
        }

    wrap:

    复制代码
    function tiWrap( call, wrapped ) {
            // nested template, using {{wrap}} tag
            var options = call.options || {};
            options.wrapped = wrapped;
            // Apply the template, which may incorporate wrapped content,
            return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item );
        }
    复制代码

    这跟6的运行模式差不多,很不幸的是,我的源码在执行这个例子的时候出错,后来我找了一段时间后发现问题,将源码修改了一下。恢复正常了。修改tiHtml方法里

    复制代码
    return jQuery.map(
                jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : jQuery.trim(wrapped) ).filter( filter || "*" ),
                function(e) {
                    return textOnly ?
                        e.innerText || e.textContent :
                        e.outerHTML || outerHtml(e);
                });
    复制代码

    7和6例子一样,在匿名函数执行的时候,重新执行了jQuery.tmpl获取了新模板的内容,生成了匿名函数,如果你们有功夫看一下生成的匿名函数,你们会发现里面都很多newItem事先定义好的方法调用,然后在执行这些匿名函数的时候,依次调用这些方法。

    8.$.tmplItem()

    例子可以看例子5,其实这个方法就很简单了。看一下源码

    复制代码
    tmplItem: function( elem ) {
                var tmplItem;
                if ( elem instanceof jQuery ) {
                    elem = elem[0];
                }
                while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}//获取data信息,用户传入的内容信息
                return tmplItem || topTmplItem;
            }
    复制代码

    可以看到这个while循环不断向上查询,因为在我们第一个例子中,我们在storeTmplItems方法中,进行一定的保存。这里就是查找到显示出来。

    9.结语

    以上基本完成了一个源码的阅读,从中学习的东西有很多,类似模板一类的框架,需要一个强大的正则解析,需要能将数据元与字符串很好结合的方法,而这个框架则是用正则生成这个方法。这个框架也提供了一些向上遍历的方式,大家都可以借鉴。这里暂时不讨论该框架的执行效率。我们以后还会接触到别的更好更强大的框架。这只是个开始。内容不多,时间刚好,这是我的读码体会,可能不全,也会有错误,希望园友们提出来,大家一起探讨学习。

  • 相关阅读:
    MySQL日志
    MySQL索引和事务
    【收集】腾讯AlloyTeam
    js基础知识点(只有点)
    【扩展】Canvas绘制列表的尝试
    开播 开博 凯博
    【总结】移动web问题小结
    〖前端开发〗HTML/CSS基础知识学习笔记
    第四次读书笔记——《代码大全》(续)
    C++笔记(1)
  • 原文地址:https://www.cnblogs.com/linaijiao/p/4615862.html
Copyright © 2011-2022 走看看