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

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. jQuery无缝循环开源多元素动画轮播jquery.slides插件

    详细内容请点击 初始化插件: 一款基于jQuery无缝轮播图插件,支持图内元素动画,可以自定义动画类型$(".slideInner").slide({slideContainer: ...

  2. 深入浅出ExtJS 第六章 布局

       6.1 布局的用途 6.1 布局的用途 //决定把什么东西放到什么位置; var vieport = new Ext.Viewport({ layout:'border', //使用Border ...

  3. 面向对象的异常处理之深入理解java异常处理机制

    什么是异常? 异常是对问题的描述,将问题的对象进行封装: 异常体系的特点:异常体系中的所有类以及建立的对象: 都具有可抛性,也就是说可以被throw和throws关键字所操作,只有异常体系具有该特点: ...

  4. C#中winform窗体如何嵌入cmd命令窗口

    解决方法一: 自己放一个文本框,改成黑色,然后输入命令,执行时,你Process.Start cmd ,此时CMD窗口不显示,然后,将CMD的返回值,再取出来,设回文本框. 如何用这种方法实时获取cm ...

  5. OSPF LSA的详解

    LSA类型的配置与查看 1基本配置 R1(config)#NO IP DO LO R1(config)#NO ENAble PAssword R1(config)#LINe COnsole 0 R1( ...

  6. [Android开发系列]IT博客应用V1.3

    首先,感谢使用这款软件并给我意见的朋友们,有你们的意见,才有了这个版本. 其次,检索功能和分类筛选功能(如果是你提的意见,记得在下面mark哦,毕竟读代码你能发现,其实发意见这个就是用自己的邮箱给自己 ...

  7. 以OpenGL方式运行Unity

    Unity在Windows上默认以DirextX的方式运行,在MacOS和Linux上默认以OpenGl的方式运行, 如果希望在Windows上以OpenGL的方式运行可以在命令行中输入 -force ...

  8. nyoj71--独木舟上的旅行

    描述 进行一次独木舟的旅行活动,独木舟可以在港口租到,并且之间没有区别.一条独木舟最多只能乘坐两个人,且乘客的总重量不能超过独木舟的最大承载量.我们要尽量减少这次活动中的花销,所以要找出可以安置所有旅 ...

  9. SQL 复杂查询

    一.子查询 .相关子查询 相关子查询是指需要引用主查询列表的子查询语句,相关子查询是通过EXISTS谓词来实现的.下面以显示工作在"new york"的所有雇员为例,说明相关子查询 ...

  10. if语句代码优化

    if($sum==7){ $sz+=135; }elseif($sum==5){ $sz+=80; }elseif($sum==6){ $sz+=97; }elseif($sum==4){ $sz+= ...