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