Handlebars模板引擎中的each巢狀及原始碼淺讀
Handlebars模板引擎作為時下最流行的模板引擎之一,已然在開發中為我們提供了無數便利。作為一款無語義的模板引擎,Handlebars只提供極少的helper函式,還原模板引擎的本身,也許這正是他在效率上略勝一籌的原因,這裡有一個網友測試,表明Handlebars在萬行效率上,稍勝jade,EJS一籌。當然,模板引擎這種東西除了效率外,開發效率,美觀度也是很重要的考評一個模板引擎優劣的指標,例如,很多開發者都覺得Jade十分簡潔、開發很爽。愚安在這裡並不想立Flag引戰。關於Handlebars為何在效率上有這樣的優勢,愚安在這裡就不繼續深入了,有興趣的童鞋可以參見一下原始碼
當然,也有不少使用者表示Handlebars提供的功能太少了,諸如
- if只能判斷condition只能為一個值,不能為一個express
- 不提供四則運算
-
不能進行索引
...
但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>
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可以在模板檔案中用
當前元素是否是第一個元素
當前元素是否是最後一個元素
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
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(資料幀)
javascript
var createFrame = function(object) {
var frame = Utils.extend({}, object);
frame._parent = object;
return frame;
};資料幀是編譯Handlebars模板檔案,非常重要的一環,他將當前上下文所有的資料封裝成一個物件,傳給當前fn,保證fn能拿到完成的上下文資料,可以看出這裡的_parent就是上文例子中路徑符可以訪問到上層資料的原因。說到這裡,Handlebars是怎麼處理這種路徑符的呢,請看:
javascript
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](https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/ast.js)是Handlebasr的compiler中非常基礎的一部分,他定義了幾種節點型別,其中IdNode就是通過路徑符轉化來的,就是上文contextPath中的id。正是IdNode的存在,才使得Handlebars在無語義的基礎上,可以適應各種形式的資料,各種形式的巢狀。<br> 注3:fn(編譯而來的編譯函式) 聽起來有點拗口,但確實是這樣一個存在,Handlebars的compiler在編譯完模板之後,會生成一個fn,將context傳入此fn,便可以得到當前上下文對應的HTML字串ret
javascript
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就達到了目的。