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...索引到),这样既保证了数据结构的轻便,又实现了应有的功能。接下来,我们从源码层去看看具体的实现。

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的实现过程,看似很简单,但又看不出什么门道。好的,既然已经把源码扒拉出来了,愚安我如果不讲讲清楚,也对不起标题中的浅读二字。
注1:contextPath(上下文路径)

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值