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

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开发从零开始填坑

    开始学习Java,感觉较.NET知识更零碎一些,所以开个帖子把自己踩过的坑记录下来,都是边边角角网上不容易找到的东西. 1.java命令格式:>cd %parent-of-pakadgePath ...

  2. php返回json数据函数例子

    json_encode()函数用法. echo json_encode(array('a'=>'bbbb','c'=>'ddddd'); 这样就会生成一个标准的json格式的数据 代码如下 ...

  3. Wim技术之Wim文件的制作

    背景:操作的镜像文件为win8.1 update的ISO里的Wim文件 1.使用如下命令将支持WimBoot的instal.Wim文件转换成可以支持wimboot启动的映像文件 Dism /Expor ...

  4. Exchange 2010先决条件

    为了方便大家一步到位的进行学习,已将各种角色安装所需的先决条件给与总结了,但注意系统需求是2008 R2     1.对于执行客户端访问.集线器传输及邮箱角色典型安装的服务器            ( ...

  5. eBay 开发流程

    1[记录]注册成为eBay开发者(eBay Developers Program)+创建Sanbox Key和Production Key http://www.crifan.com/register ...

  6. 【学习笔记】【C语言】变量类型

    根据变量的作用域,可以分为: 1.局部变量: 1> 定义:在函数(代码块)内部定义的变量(包括函数的形参) 2> 作用域:从定义变量的那一行开始,一直到代码块结束 3> 生命周期:从 ...

  7. 【学习笔记】【C语言】赋值运算

    将某一数值赋给某个变量的过程,称为赋值. 1. 简单赋值 C语言规定,变量要先定义才能使用,也可以将定义和赋值在同一个语句中进行 int a = 10 + 5;的运算过程 a = b = 10;的运算 ...

  8. Base Pattern基本模式_Gateway入口

    •Gateway入口 ◦一个封装了对外部系统或资源访问的对象. ◾OO系统中,也需要访问一些不是对象的事物,DB表,XML,事务. ◾这些外部资源的API很复杂. ◾入口类对象将简单的方法调用转换成相 ...

  9. path 环境变量

    path(环境变量)是dos以前的内部命令,windows继续沿用至今.用作运行某个命令的时候,本地查找不到某个命令或文件,会到这个声明的目录中去查找.一般设定java的时候为了在任何目录下都可以运行 ...

  10. cass实体编码列表

    地物名称 编码 图层 类别 参数一 参数二 实体类型 三角点 131100 KZD 20 gc113 3 SPECIAL,1 三角点分数线 131110 KZD 附 LINE 三角点高程注记 1311 ...