您的位置:首页 > Web前端 > JQuery

jQuery-template.js学习

2016-02-04 23:48 459 查看
原文 http://www.cnblogs.com/wumadi/p/3443471.html
主题 jQuery HTML

花了点时间,看了下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 /article/1255501.html

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

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


<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,这算是这个插件比较重要的一个部分。传入参数则是模版字符串

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编译成模版

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表示当前数据

<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}}

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>


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

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

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


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

{{each(i,lang) Langs}}

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

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

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

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占位符

<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}}

<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,来看一下这两个方法

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


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.结语

以上基本完成了一个源码的阅读,从中学习的东西有很多,类似模板一类的框架,需要一个强大的正则解析,需要能将数据元与字符串很好结合的方法,而这个框架则是用正则生成这个方法。这个框架也提供了一些向上遍历的方式,大家都可以借鉴。这里暂时不讨论该框架的执行效率。我们以后还会接触到别的更好更强大的框架。这只是个开始。内容不多,时间刚好,这是我的读码体会,可能不全,也会有错误,希望园友们提出来,大家一起探讨学习。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: