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