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

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中打断点看调用栈,感觉阅读速度还行,想读源码的童鞋可以试一下-

Handlebars模板引擎中的each嵌套及源码浅读的更多相关文章

  1. Handlebars模板引擎之高阶

    Helpers 其实在Handlebars模板引擎之进阶我想说if else的功能的,可是由于这个功能在我的开发中我觉的鸡肋没啥用,就直接不用了. 因为if else只能进行简单判断,如果条件参数返回 ...

  2. handlebars模板引擎使用初探1

    谈到handlebars,我们不禁产生疑问,为什么要使用这样的一个工具呢?它究竟能为我们带来什么样的好处?如何使用它呢? 一.handlebars可以干什么? 首先,我们来看一个案例: 有这样的htm ...

  3. Layui 模板引擎中的 日期格式化

    原文:https://www.jianshu.com/p/948a474b5ed7 原文:https://blog.csdn.net/DCFANS/article/details/92064112 模 ...

  4. [ASPX] 模版引擎XTemplate与代码生成器XCoder(源码)

    模版引擎XTemplate是一个仿T4设计的引擎,功能上基本与T4一致(模版语法上完全兼容T4,模版头指令部分兼容). 自己设计模版引擎,就是为了代码生成器.网站模版.邮件模版等多种场合,也就是要能拿 ...

  5. Fabric2.2中的Raft共识模块源码分析

    引言 Hyperledger Fabric是当前比较流行的一种联盟链系统,它隶属于Linux基金会在2015年创建的超级账本项目且是这个项目最重要的一个子项目.目前,与Hyperledger的另外几个 ...

  6. eclipse中tomcat调试正确关联源码

    1.build path中jar包关联本地源码 2.tomcat中添加source关联工程lib下的jar包 以上两步即可. 可解决tomcat直接关联本地源码debug时无法计算表达式的情况. 错误 ...

  7. 动态语言切换(续)-designer中的retranslateUi(带源码)

    本站所有文章由本站和原作者保留一切权力,仅在保留本版权信息.原文链接.原文作者的情况下允许转载,转载请勿删改原文内容, 并不得用于商业用途. 谢谢合作.原文链接:动态语言切换(续)-designer中 ...

  8. RocketMQ中Broker的HA策略源码分析

    Broker的HA策略分为两部分①同步元数据②同步消息数据 同步元数据 在Slave启动时,会启动一个定时任务用来从master同步元数据 if (role == BrokerRole.SLAVE) ...

  9. 【转】在Express项目中使用Handlebars模板引擎

    原文:http://fraserxu.me/2013/09/12/Using-Handlebarsjs-with-Expressjs/ 最近在用Expressjs做一个项目,前后端都用它来完成.自己之 ...

随机推荐

  1. java使用POI jar包读写xls文件

    主要使用poi jar来操作excel文件.代码中用到的数据库表信息见ORACLE之表.使用public ArrayList<Person> getPersonAllRecords()获得 ...

  2. .NET DLL 保护措施详解(二)关于性能的测试

    先说结果: 加了缓存的结果与C#原生代码差异不大了 我对三种方式进行了测试: 第一种,每次调用均动态编译 第二种,缓存编译好的对象 第三种,直接调用原生C#代码 .net dll保护系列 ------ ...

  3. CSS导航指示箭头

    效果图 代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www. ...

  4. C语言经典参考书籍

    <C程序设计语言> Brian W.Kernighan,Dennis M.Ritchie 编著:C语言的开山之作.C程序员应该人手一本. <C语言参考手册> Samuel P. ...

  5. Android处理Bitmap使其能够不失真等比缩放裁剪后显示在ImageView上

    Android开发过程中,我们有时需要动态得显示一些图片,并且这些图片的大小差距会十分大,如果需求并不是需要图片完整显示,但是需要不失真,并且要图片中间部分的情况下,我们需要做一系列处理,因为这个时候 ...

  6. ORACLE的分组统计之ROLLUP(一)

    Oracle 9i以后,扩展了group by 的功能,能够满足大部分多维数据的分析统计功能,主要表现: 1. rollup,cube,grouping sets 扩展group by字句提供了丰富的 ...

  7. Part 4 Identity Column in SQL Server

    Identity Column in SQL Server If a column is marked as an identity column, then the values for this ...

  8. ArrayList与List对象用法与区别

    比如: 代码如下 复制代码 string[] s=new string[3];//赋值s[0]="a";s[1]="b";s[2]="c"; ...

  9. Server.UrlEncode、HttpUtility.UrlDecode的区别

    Server.UrlEncode.HttpUtility.UrlDecode的区别 在对URL进行编码时,该用哪一个?这两都使用上有什么区别吗? 测试: string file="文件上(传 ...

  10. centos下安装图像化界面

    前面我们安装的centos系统多为没有图像化界面的命令行界面,为了安装oracle等工具,我们先为我们的centos安装图像化界面 使用命令为 yum groupinstall "Deskt ...