无码帝 阅读(56) 评论(0)

花了点时间,看了下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( /[\r\t\n]/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][^<]*)?(<[\w\W]+>)([^>]*[^>\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
    *           <[\w\W]+>                            --匹配<,任意字符(至少一个,优先匹配),>
    *       )
    *       (                                        --匹配捕获                                 ($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 = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /

这个比较简单,留给读者吧,呵呵。匹配结果当然是不满足,我们使用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.结语

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