jQuery-template.js學習


花了點時間,看了下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.結語

以上基本完成了一個源碼的閱讀,從中學習的東西有很多,類似模板一類的框架,需要一個強大的正則解析,需要能將數據元與字符串很好結合的方法,而這個框架則是用正則生成這個方法。這個框架也提供了一些向上遍歷的方式,大家都可以借鑒。這里暫時不討論該框架的執行效率。我們以后還會接觸到別的更好更強大的框架。這只是個開始。內容不多,時間剛好,這是我的讀碼體會,可能不全,也會有錯誤,希望園友們提出來,大家一起探討學習。


免責聲明!

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



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