Handlebars模板引擎中的each嵌套及源码浅读

若显示效果不佳,可移步到愚安的小窝

Handlebars模板引擎作为时下最流行的模板引擎之一,已然在开发中为我们提供了无数便利。作为一款无语义的模板引擎,Handlebars只提供极少的helper函数,还原模板引擎的本身,也许这正是他在效率上略胜一筹的原因,这里有一个网友测试,表明Handlebars在万行效率上,稍胜jade,EJS一筹。当然,模板引擎这种东西除了效率外,开发效率,美观度也是很重要的考评一个模板引擎优劣的指标,例如,很多开发者都觉得Jade十分简洁、开发很爽。愚安在这里并不想立Flag引战。关于Handlebars为何在效率上有这样的优势,愚安在这里就不继续深入了,有兴趣的童鞋可以参见一下源码

当然,也有不少用户表示Handlebars提供的功能太少了,诸如

  1. if只能判断condition只能为一个值,不能为一个express
  2. 不提供四则运算
  3. 不能进行索引

    ...

    但Handlebars为我们提供了registerHelper函数,让我们可以轻松注册一些Helper去扩展Handlebars,辅助我们更快的开发。当然Handlebars也为我们提供内置了几个Helper,如each,if,else,with等,其中each作为唯一内置的循环helper在模板编写的过程中有诸多可以发挥的地方。
1.循环数组
var arr = [
{name:'John',age:11},
{name:'Amy',age:12},
{name:'Lucy',age:11}
];
<ol>
{{#each arr}}
<li>Name:{{name}},Age:{{age}}<li>
{{/each}}
</ol>

输出结果为:

<ol>
<li>Name:John,Age:11<li>
<li>Name:Amy,Age:12<li>
<li>Name:Lucy,Age:11<li>
</ol>

这是一个非常普通的数组循环输出

2.循环对象
var obj = {
name: 'John',
age: 11,
sex: 'male',
id: '000001'
};
<ul>
{{#each obj}}
<li>{{@key}}:{{this}}<li>
{{/each}}
</ul>

输出结果为:

<ul>
<li>name:John<li>
<li>age:11<li>
<li>sex:male<li>
<li>id:000001<li>
</ul>

这里的@key就是对象里的属性名,关于原理,后面的代码解读里会稍作解释

3.内部嵌套循环
var list = [
{name:'John',sports:'basketball',scores:[2,2,2,2]},
{name:'Amy',sports:'tennis',scores:[1,2,3,4]}
];
<table>
{{#each list}}
<tr>
<td>{{name}}</td>
<td>
{{#each scores}}
{{../sports}}:{{this}}<br/>
{{/each}}
</td>
</tr>
{{/each}}
</table>

输出结果为:

<table>
<tr>
<td>John</td>
<td>
basketball:2<br/>
basketball:2<br/>
basketball:2<br/>
basketball:2<br/>
</td>
</tr>
<tr>
<td>Amy</td>
<td>
tennis:1<br/>
tennis:2<br/>
tennis:3<br/>
tennis:4<br/>
</td>
</tr>
</table>

这里是一个嵌套循环,第一个each循环list层,属性有name,sports,scores,在第二个each循环scores,此时的this指向数组scores里的每个score,{{../sports}}指向上层结构中的sports。

在同一个对象中,访问上层数据,仿佛很好理解,如保留一个最上层的引用,向下寻找。但其实,这里的路径层次并不是在一个对象里的层次关系(应该不是只有我一个人这么认为的吧),而是多个each循环的嵌套层次,下个例子中就可以看出。

4.多重嵌套循环

这里有一个全国各省、直辖市的地名与邮编的数组zone,还有一个区域划分的数组catZone。

var zone = [{"label":"北京","code":110000},
{"label":"天津","code":120000},
{"label":"河北","code":130000},
{"label":"山西","code":140000},
{"label":"内蒙古","code":150000},
{"label":"辽宁","code":210000},
{"label":"吉林","code":220000},
{"label":"黑龙江","code":230000},
{"label":"上海","code":310000},
{"label":"江苏","code":320000},
{"label":"浙江","code":330000},
{"label":"安徽","code":340000},
{"label":"福建","code":350000},
{"label":"江西","code":360000},
{"label":"山东","code":370000},
{"label":"河南","code":410000},
{"label":"湖北","code":420000},
{"label":"湖南","code":430000},
{"label":"广东","code":440000},
{"label":"广西","code":450000},
{"label":"海南","code":460000},
{"label":"重庆","code":500000},
{"label":"四川","code":510000},
{"label":"贵州","code":520000},
{"label":"云南","code":530000},
{"label":"*","code":540000},
{"label":"陕西","code":610000},
{"label":"甘肃","code":620000},
{"label":"青海","code":630000},
{"label":"宁夏","code":640000},
{"label":"*","code":650000},
{"label":"*","code":710000},
{"label":"香港","code":810000},
{"label":"澳门","code":820000}
];
var catZone = [{'label':"江浙沪",'code':[310000,320000,330000]},
{'label':"华东",'code':[340000,360000]},
{'label':"华北",'code':[110000,120000,130000,140000,150000]},
{'label':"华中",'code':[410000,420000,430000]},
{'label':"华南",'code':[350000,440000,450000,460000]},
{'label':"东北",'code':[210000,220000,230000]},
{'label':"西北",'code':[610000,620000,630000,640000,650000]},
{'label':"西南",'code':[500000,510000,520000,530000,540000]},
{'label':"港澳台",'code':[810000,820000,710000]}
];
var data = {zone:zone,catZone:catZone};

现在希望将各个地名按区域做成表格,首先,需要循环catZone,拿出其中的code数组并进行第二个,然后去zone数组中去找code为当前code的地名,但code并不是索引,无法直接得到,所以继续循环遍历zone(此时 ../../zone 为data中的zone),比较zone中code与第二个each循环中code(../this 指向上层的this)是否相等。

	<table class="table table-bordered">
{{! 第一个each循环}}
{{#each catZone}}
<tr>
<th><label><input type="checkbox" />{{label}}</label></th>
<td>
{{! 第二个each循环}}
{{#each code}}
{{! 第三个each循环}}
{{#each ../../zone}}
{{! equal为自定义helper,比较两个参数是否相等,否则options.inverse}}
{{#equal code ../this}}
<label class='pull-left'><input type="checkbox" data-code="{{code}}"/>
{{label}}
</label>
{{/equal}}
{{/each}}
{{/each}}
</td>
</tr>
{{/each}}
</table>

最终效果如下:

江浙沪

上海

江苏

浙江

华东

安徽

江西

华北

北京

天津

河北

山西

内蒙古

华中

河南

湖北

湖南

华南

福建

广东

广西

海南

东北

辽宁

吉林

黑龙江

西北

陕西

甘肃

青海

宁夏

*

西南

重庆

四川

贵州

云南

*

港澳台

香港

澳门

*

####源码解读
从上面的例子可以看出,自带的Helper:each是一个非常强大辅助函数。不但可以循环遍历数组和对象,而且支持一种以路径符来表示的嵌套关系。从最后一个例子可以看出,这种嵌套索引并不是一种从顶层向下的关系,而是从当前层出发,寻觅上层数据的做法(记为当前层的parent,多层可以通过parent.parent.parent...索引到),这样既保证了数据结构的轻便,又实现了应有的功能。接下来,我们从源码层去看看具体的实现。

```javascript
instance.registerHelper('each', function(context, options) {
if (!options) {
throw new Exception('Must pass iterator to #each');
}

var fn = options.fn,
inverse = options.inverse;
var i = 0,
ret = "",
data; var contextPath;
if (options.data && options.ids) {
//注1
contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.';
}
//若传参为一个function,则context = context()
if (isFunction(context)) {
context = context.call(this);
} if (options.data) {
//注2
data = createFrame(options.data);
} if (context && typeof context === 'object') {
//如果上下文(参数)为数组
if (isArray(context)) {
for (var j = context.length; i < j; i++) {
if (data) {
//index,fitst,last可以在模板文件中用
//@index 当前元素在上下文中的索引
//@first 当前元素是否是第一个元素
//@last 当前元素是否是最后一个元素
data.index = i;
data.first = (i === 0);
data.last = (i === (context.length - 1));
//构建形如contextPath.1的上下文路径
if (contextPath) {
data.contextPath = contextPath + i;
}
}
//合并所有ret,构成渲染之后的html字符串
//注3
ret = ret + fn(context[i], {
data: data
});
}
} else {
//上下文为object时,用for..in遍历object
for (var key in context) {
//剔除原型链属性
if (context.hasOwnProperty(key)) {
if (data) {
//同上
//@key是当context为object时,当前属性的key
data.key = key;
data.index = i;
data.first = (i === 0);
//构建形如contextPath.key的上下文路径
if (contextPath) {
data.contextPath = contextPath + key;
}
}
ret = ret + fn(context[key], {
data: data
});
i++;
}
}
}
}
//若each中没有可以渲染的内容,执行inverse方法
if (i === 0) {
ret = inverse(this);
} return ret;

});

上面的代码就是原生each-helper的实现过程,看似很简单,但又看不出什么门道。好的,既然已经把源码扒拉出来了,愚安我如果不讲讲清楚,也对不起标题中的浅读二字。<br>
注1:contextPath(上下文路径)
```javascript
function appendContextPath(contextPath, id) {
return (contextPath ? contextPath + '.' : '') + id;
}

appendContextPath方法在最终会在当前层的data上构造一个这样的contextPath = id.index.id.index.id.index....(index为Array索引或Object的key)

注2:frame(数据帧)

 var createFrame = function(object) {
var frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};

数据帧是编译Handlebars模板文件,非常重要的一环,他将当前上下文所有的数据封装成一个对象,传给当前fn,保证fn能拿到完成的上下文数据,可以看出这里的_parent就是上文例子中路径符可以访问到上层数据的原因。说到这里,Handlebars是怎么处理这种路径符的呢,请看:

var AST = {
/*省略*/
IdNode: function(parts, locInfo) {
LocationInfo.call(this, locInfo);
this.type = "ID"; var original = "",
dig = [],
depth = 0,
depthString = ''; for (var i = 0, l = parts.length; i < l; i++) {
var part = parts[i].part;
original += (parts[i].separator || '') + part; if (part === ".." || part === "." || part === "this") {
if (dig.length > 0) {
throw new Exception("Invalid path: " + original, this);
} else if (part === "..") {
depth++;
depthString += '../';
} else {
this.isScoped = true;
}
} else {
dig.push(part);
}
} this.original = original;
this.parts = dig;
this.string = dig.join('.');
this.depth = depth;
this.idName = depthString + this.string; // an ID is simple if it only has one part, and that part is not
// `..` or `this`.
this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; this.stringModeValue = this.string;
}
/*省略*/
};

AST是Handlebasr的compiler中非常基础的一部分,他定义了几种节点类型,其中IdNode就是通过路径符转化来的,就是上文contextPath中的id。正是IdNode的存在,才使得Handlebars在无语义的基础上,可以适应各种形式的数据,各种形式的嵌套。

注3:fn(编译而来的编译函数)

听起来有点拗口,但确实是这样一个存在,Handlebars的compiler在编译完模板之后,会生成一个fn,将context传入此fn,便可以得到当前上下文对应的HTML字符串ret

var fn = this.createFunctionContext(asObject);
JavaScriptCompiler.prototype = {
/*省略*/
createFunctionContext: function(asObject) {
var varDeclarations = ''; var locals = this.stackVars.concat(this.registers.list);
if (locals.length > 0) {
varDeclarations += ", " + locals.join(", ");
} // Generate minimizer alias mappings
for (var alias in this.aliases) {
if (this.aliases.hasOwnProperty(alias)) {
varDeclarations += ', ' + alias + '=' + this.aliases[alias];
}
} var params = ["depth0", "helpers", "partials", "data"]; if (this.useDepths) {
params.push('depths');
} // Perform a second pass over the output to merge content when possible
var source = this.mergeSource(varDeclarations);
if (asObject) {
params.push(source); return Function.apply(this, params);
} else {
return 'function(' + params.join(',') + ') {\n ' + source + '}';
}
}
}

这里具体的代码戳这里,编译本身是个很复杂的事情,既需要有清晰的结构,完整的规范,又要有一定的优化和冗余手段,我在这里就不讲了(其实我也不懂,555~)。可以看出createFunctionContext返回值为一个编译之后的Function就达到了目的。

结语

现在前端技术发展迅速,对模板引擎的要求越来越高,功能越来越复杂。Handlebars是愚安我非常喜欢的一款模板引擎,也算是第一个决定去读源码的引擎(相当吃力),在阅读的过程中,愚安我是一边看源码,一边在chrome中打断点看调用栈,感觉阅读速度还行,想读源码的童鞋可以试一下-

上一篇:Hibernate查询之SQL查询


下一篇:Hibernate框架之Criteria查询