underscore 系列之實現一個模板引擎(上)

前言

underscore 提供了模板引擎的功能,舉個例子:javascript

var tpl = "hello: <%= name %>";

var compiled = _.template(tpl);
compiled({name: 'Kevin'}); // "hello: Kevin"
複製代碼

感受好像沒有什麼強大的地方,再來舉個例子:html

在 HTML 文件中:java

<ul id="name_list"></ul>

<script type="text/html" id="user_tmpl"> <%for ( var i = 0; i < users.length; i++ ) { %> <li> <a href="<%=users[i].url%>"> <%=users[i].name%> </a> </li> <% } %> </script>
複製代碼

JavaScript 文件中:git

var container = document.getElementById("user_tmpl");

var data = {
    users: [
        { "name": "Kevin", "url": "http://localhost" },
        { "name": "Daisy", "url": "http://localhost" },
        { "name": "Kelly", "url": "http://localhost" }
    ]
}
var precompile = _.template(document.getElementById("user_tmpl").innerHTML);
var html = precompile(data);

container.innerHTML = html;
複製代碼

效果爲:github

模板引擎效果

那麼該如何實現這樣一個 _.template 函數呢?bash

實現思路

underscore 的 template 函數參考了 jQuery 的做者 John Resig 在 2008 年發表的一篇文章 JavaScript Micro-Templating,咱們先從這篇文章的思路出發,思考一下如何寫一個簡單的模板引擎。數據結構

依然是以這段模板字符串爲例:閉包

<%for ( var i = 0; i < users.length; i++ ) { %>
    <li> <a href="<%=users[i].url%>"> <%=users[i].name%> </a> </li>
<% } %>
複製代碼

John Resig 的思路是將這段代碼轉換爲這樣一段程序:架構

// 模擬數據
var users = [{"name": "Kevin", "url": "http://localhost"}];

var p = [];
for (var i = 0; i < users.length; i++) {
    p.push('<li><a href="');
    p.push(users[i].url);
    p.push('">');
    p.push(users[i].name);
    p.push('</a></li>');
}

// 最後 join 一下就能夠獲得最終拼接好的模板字符串
console.log(p.join('')) // <li><a href="http://localhost">Kevin</a></li>
複製代碼

咱們注意,模板實際上是一段字符串,咱們怎麼根據一段字符串生成一段代碼呢?很容易就想到用 eval,那咱們就先用 eval 吧。函數

而後咱們會發現,爲了轉換成這樣一段代碼,咱們須要將<%xxx%>轉換爲 xxx,其實就是去掉包裹的符號,還要將 <%=xxx%>轉化成 p.push(xxx),這些均可以用正則實現,可是咱們還須要寫 p.push('<li><a href="');p.push('">');吶,這些該如何實現呢?

那咱們換個思路,依然是用正則,可是咱們

  1. %> 替換成 p.push('
  2. <% 替換成 ');
  3. <%=xxx%> 替換成 ');p.push(xxx);p.push('

咱們來舉個例子:

<%for ( var i = 0; i < users.length; i++ ) { %>
    <li>
        <a href="<%=users[i].url%>">
            <%=users[i].name%>
        </a>
    </li>
<% } %>
複製代碼

按照這個替換規則會被替換爲:

');for ( var i = 0; i < users.length; i++ ) { p.push('
    <li>
        <a href="');p.push(users[i].url);p.push('">
            ');p.push(users[i].name);p.push('
        </a>
    </li>
'); } p.push('
複製代碼

這樣確定會報錯,畢竟代碼都沒有寫全,咱們在首和尾加上部分代碼,變成:

// 添加的首部代碼
var p = []; p.push(' ');for ( var i = 0; i < users.length; i++ ) { p.push(' <li> <a href="');p.push(users[i].url);p.push('"> ');p.push(users[i].name);p.push(' </a> </li> '); } p.push(' // 添加的尾部代碼 ');
複製代碼

咱們整理下這段代碼:

var p = []; p.push('');
for ( var i = 0; i < users.length; i++ ) { 
    p.push('<li><a href="');
    p.push(users[i].url);
    p.push('">');
    p.push(users[i].name);
    p.push('</a></li>'); 
}
    p.push('');
複製代碼

剛好能夠實現這個功能,不過還要注意一點,要將換行符替換成空格,防止解析成代碼的時候報錯,不過在這裏爲了方便理解原理,就只在代碼裏實現。

初版

咱們來嘗試實現初版:

// 初版
function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var string = "var p = []; p.push('" +
    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');"

    eval(string)

    return p.join('');
};
複製代碼

爲了驗證是否有用:

HTML 文件:

<script type="text/html" id="user_tmpl"> <%for ( var i = 0; i < users.length; i++ ) { %> <li> <a href="<%=users[i].url%>"> <%=users[i].name%> </a> </li> <% } %> </script>
複製代碼

JavaScript 文件:

var users = [
    { "name": "Byron", "url": "http://localhost" },
    { "name": "Casper", "url": "http://localhost" },
    { "name": "Frank", "url": "http://localhost" }
]
tmpl("user_tmpl", users)
複製代碼

完整的 Demo 能夠查看 template 示例一

Function

在這裏咱們使用了 eval ,實際上 John Resig 在文章中使用的是 Function 構造函數。

Function 構造函數建立一個新的 Function 對象。 在 JavaScript 中, 每一個函數實際上都是一個 Function 對象。

使用方法爲:

new Function ([arg1[, arg2[, ...argN]],] functionBody)
複製代碼

arg1, arg2, ... argN 表示函數用到的參數,functionBody 表示一個含有包括函數定義的 JavaScript 語句的字符串。

舉個例子:

var adder = new Function("a", "b", "return a + b");

adder(2, 6); // 8
複製代碼

那麼 John Resig 究竟是如何實現的呢?

第二版

使用 Function 構造函數:

// 第二版
function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    "var p = []; p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');return p.join('');");

    return fn(data);
};
複製代碼

使用方法依然跟初版相同,具體 Demo 能夠查看 template 示例二

不過值得注意的是:其實 tmpl 函數沒有必要傳入 data 參數,也沒有必要在最後 return 的時候,傳入 data 參數,即便你把這兩個參數都去掉,代碼仍是能夠正常執行的。

這是由於:

使用Function構造器生成的函數,並不會在建立它們的上下文中建立閉包;它們通常在全局做用域中被建立。當運行這些函數的時候,它們只能訪問本身的本地變量和全局變量,不能訪問Function構造器被調用生成的上下文的做用域。這和使用帶有函數表達式代碼的 eval 不一樣。

這裏之因此依然傳入了 data 參數,是爲了下一版作準備。

with

如今有一個小問題,就是實際上咱們傳入的數據結構可能比較複雜,好比:

var data = {
    status: 200,
    name: 'kevin',
    friends: [...]
}
複製代碼

若是咱們將這個數據結構傳入 tmpl 函數中,在模板字符串中,若是要用到某個數據,老是須要使用 data.namedata.friends 的形式來獲取,麻煩就麻煩在我想直接使用 name、friends 等變量,而不是繁瑣的使用 data. 來獲取。

這又該如何實現的呢?答案是 with。

with 語句能夠擴展一個語句的做用域鏈(scope chain)。當須要屢次訪問一個對象的時候,可使用 with 作簡化。好比:

var hostName = location.hostname;
var url = location.href;

// 使用 with
with(location){
    var hostname = hostname;
    var url = href;
}
複製代碼
function Person(){
    this.name = 'Kevin';
    this.age = '18';
}

var person = new Person();

with(person) {
    console.log('my name is ' + name + ', age is ' + age + '.')
}
// my name is Kevin, age is 18.
複製代碼

最後:不建議使用 with 語句,由於它多是混淆錯誤和兼容性問題的根源,除此以外,也會形成性能低下

第三版

使用 with ,咱們再寫一版代碼:

// 第三版
function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    // 其實就是這裏多添加了一句 with(obj){...}
    "var p = []; with(obj){p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');}return p.join('');");

    return fn(data);
};
複製代碼

具體 Demo 能夠查看 template 示例三

第四版

若是咱們的模板不變,數據卻發生了變化,若是使用咱們的以前寫的 tmpl 函數,每次都會 new Function,這實際上是沒有必要的,若是咱們能在使用 tmpl 的時候,返回一個函數,而後使用該函數,傳入不一樣的數據,只根據數據不一樣渲染不一樣的 html 字符串,就能夠避免這種無謂的損失。

// 第四版
function tmpl(str, data) {
    var str = document.getElementById(str).innerHTML;

    var fn = new Function("obj",

    "var p = []; with(obj){p.push('" +

    str
    .replace(/[\r\t\n]/g, "")
    .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
    .replace(/<%/g, "');")
    .replace(/%>/g,"p.push('")
    + "');}return p.join('');");

    var template = function(data) {
        return fn.call(this, data)
    }
    return template;
};

// 使用時
var compiled = tmpl("user_tmpl");
results.innerHTML = compiled(data);
複製代碼

具體 Demo 能夠查看 template 示例四

下期預告

至此,咱們已經跟着 jQuery 的做者 John Resig 實現了一個簡單的模板引擎,雖然 underscore 基於這個思路實現,可是功能強大,相對的,代碼也更加複雜一下,下一篇,咱們一塊兒去分析 underscore 的 template 函數實現。

underscore 系列

underscore 系列目錄地址:github.com/mqyqingfeng…

underscore 系列預計寫八篇左右,重點介紹 underscore 中的代碼架構、鏈式調用、內部函數、模板引擎等內容,旨在幫助你們閱讀源碼,以及寫出本身的 undercore。

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。

相關文章
相關標籤/搜索