天天看點

Backbone入門指南(二):依賴庫Underscore

5. Underscore.js

  Underscore封裝了常用的JavaScript對象操作方法,用于提高開發效率。它本身與我們介紹的主題“Backbone”沒有半毛錢的關系,是以你可以完全不理會“Backbone”的概念來學習它,或将它單獨運用到任何一個頁面。(另外,Underscore還可以被使用在Node.js運作環境。)

  在學習Underscore之前,你應該先儲存它的API位址,因為你将在以後經常通路它:

  http://documentcloud.github.com/underscore/

  從API中,你已經可以看出,Underscore沒有任何複雜的結構和流程,它僅僅提供了一系列常用的函數。如果你将API中的方法從頭至尾用一遍,你就會對它非常了解。

  盡管如此,但我覺得還是有必要将一些重要的方法拿出來與大家讨論,它們十分重要,卻在API中描述地還不夠清楚。

5.1 Underscore對象封裝

  Underscore并沒有在原生的JavaScript對象原型中進行擴充,而是像jQuery一樣,将資料封裝在一個自定義對象中(下文中稱“Underscore對象”)。

  你可以通過調用一個Underscore對象的value()方法來擷取原生的JavaScript資料,例如:

// 定義一個JavaScript内置對象
var jsData = {
    name : 'data'
}

// 通過_()方法将對象建立為一個Underscore對象
// underscoreData對象的原型中包含了Underscore中定義的所有方法,你可以任意使用
var underscoreData = _(jsData);

// 通過value方法擷取原生資料, 即jsData
underscoreData.value();      

5.2 優先調用JavaScript 1.6内置方法

  Underscore中有許多方法在JavaScript1.6中已經被納入規範,是以在Underscore對象内部,會優先調用宿主環境提供的内置方法(如果宿主環境已經實作了這些方法),以此提高函數的執行效率。

  而對于不支援JavaScript 1.6的宿主環境,Underscore會通過自己的方式實作,而對開發者來說,這些完全是透明的。

  這裡所說的宿主環境,可能是Node.js運作環境,或用戶端浏覽器。

5.3 改變命名空間

  Underscore預設使用_(下劃線)來通路和建立對象,但這個名字可能不符合我們的命名規範,或容易引起命名沖突。

  我們可以通過noConflict()方法來改變Underscore的命名,并恢複_(下劃線)變量之前的值,例如:

<script type="text/javascript">
    var _ = '自定義變量';
</script>
<script type="text/javascript" src="underscore/underscore-min.js"></script>
<script type="text/javascript">
    // Underscore對象
    console.dir(_);
    // 将Underscore對象重命名為us, 後面都通過us來通路和建立Underscore對象
    var us = _.noConflict();
    // 輸出"自定義變量"
    console.dir(_);
</script>      

5.4 鍊式操作

  還記得我們在jQuery中是如何進行連結操作嗎?例如:

$('a')
    .css('position', 'relative')
    .attr('href', '#')
    .show();      

  Underscore同樣支援鍊式操作,但你需要先調用chain()方法進行聲明:

var arr = [10, 20, 30];
_(arr)
    .chain()
    .map(function(item){ return item++; })
    .first()
    .value();      

  如果調用了chain()方法,Underscore會将所調用的方法封裝在一個閉包内,并将傳回值封裝為一個Underscore對象并傳回:

// 這是Underscore中實作鍊式操作的關鍵函數,它将傳回值封裝為一個新的Underscore對象,并再次調用chain()方法,為方法鍊中的下一個函數提供支援。
var result = function(obj, chain) {
    return chain ? _(obj).chain() : obj;
}      

5.5 擴充Underscore

  我們可以通過mixin()方法輕松地向Underscore中擴充自定義方法,例如:

_.mixin({
    method1: function(object) {
		// todo
    },
    method2: function(arr) {
		// todo
    },
    method3: function(fn) {
		// todo
    }
});      

  這些方法被追加到Underscore的原型對象中,所有建立的Underscore對象都可以使用這些方法,它們享有和其它方法同樣的環境。

5.6 周遊集合

  each()和map()方法是最常用用到的兩個方法,它們用于疊代一個集合(數組或對象),并依次處理集合中的每一個元素,例如:

var arr = [1, 2, 3];

_(arr).map(function(item, i) {
    arr[i] = item + 1;
});

var obj = {
    first : 1,
    second : 2
}

_(obj).each(function(value, key) {
    return obj[key] = value + 1;
});      

  map()方法與each()方法的作用、參數相同,但它會将每次疊代函數傳回的結果記錄到一個新的數組并傳回。

5.7 函數節流

  函數節流是指控制一個函數的執行頻率或間隔(就像控制水流的閘門一樣),Underscore提供了debounce()和throttle()兩個方法用于函數節流。

  為了更清楚地描述這兩個方法,假設我們需要實作兩個需求:

  需求1:當使用者在文本框輸入搜尋條件時,自動查詢比對的關鍵字并提示給使用者(就像在Tmall輸入搜尋關鍵字時那樣)

  首先分析第1個需求,我們可以綁定文本框的keypress事件,當輸入框内容發生變化時,查詢比對關鍵字并展示。假設我想查詢“windows phone”,它包含13個字元,而我輸入完成隻花了1秒鐘(好像有點快,就意思意思吧),那麼在這1秒内,調用了13次查詢方法。這是一件非常恐怖的事情,如果Tmall也這樣實作,我擔心它會不會在光棍節到來之前就挂掉了(當然,它并沒有這麼脆弱,但這絕對不是最好的方案)

  更好的方法是,我們希望使用者已經輸入完成,或者正在等待提示(也許他懶得再輸入後面的内容)的時候,再查詢比對關鍵字。

  最後我們發現,在我們期望的這兩種情況下,使用者會暫時停止輸入,于是我們決定在使用者暫停輸入200毫秒後再進行查詢(如果使用者在不斷地輸入内容,那麼我們認為他可能很明确自己想要的關鍵字,是以等一等再提示他)

  這時,利用Underscore中的debounce()函數,我們可以輕松實作這個需求:

<input type="text" id="search" name="search" />
<script type="text/javascript">
    var query = _(function() {
        // 在這裡進行查詢操作
    }).debounce(200);

    $('#search').bind('keypress', query);
</script>      

  你能看到,我們的代碼非常簡潔,節流控制在debounce()方法中已經被實作,我們隻告訴它當query函數在200毫秒内沒有被調用過的話,就執行我們的查詢操作,然後再将query函數綁定到輸入框的keypress事件。

  query函數是怎麼來的?我們在調用debounce()方法時,會傳遞一個執行查詢操作的函數和一個時間(毫秒數),debounce()方法會根據我們傳遞的時間對函數進行節流控制,并傳回一個新的函數(即query函數),我們可以放心大膽地調用query函數,而debounce()方法會按要求幫我們做好控制。

  需求2:當使用者拖動浏覽器滾動條時,調用伺服器接口檢查是否有新的内容

  再來分析第2個需求,我們可以将查詢方法綁定到window.onscroll事件,但這顯然不是一個好的做法,因為使用者拖動一次滾動條可能會觸發幾十次甚至上百次onscroll事件。

  我們是否可以使用上面的debounce()方法來進行節流控制?當使用者拖動滾動條完畢後,再查詢新的内容?但這與需求不符,使用者希望在拖動的過程中也能看到新内容的變化。

  是以我們決定這樣做:使用者在拖動時,每兩次查詢的間隔不少于500毫秒,如果使用者拖動了1秒鐘,這可能會觸發200次onscroll事件,但我們最多隻進行2次查詢。

  利用Underscore中的throttle()方法,我們也可以輕松實作這個需求:

<script type="text/javascript">
    var query = _(function() {
        // 在這裡進行查詢操作
    }).throttle(500);

    $(window).bind('scroll', query);
</script>      

  代碼仍然十分簡潔,因為在throttle()方法内部,已經為我們實作的所有控制。

  你可能已經發現,debounce()和throttle()兩個方法非常相似(包括調用方式和傳回值),作用卻又有不同。

  它們都是用于函數節流,控制函數不被頻繁地調用,節省用戶端及伺服器資源。

  • debounce()方法關注函數執行的間隔,即函數兩次的調用時間不能小于指定時間。
  • throttle()方法更關注函數的執行頻率,即在指定頻率内函數隻會被調用一次。

5.8 模闆解析

  Underscore提供了一個輕量級的模闆解析函數,它可以幫助我們有效地組織頁面結構和邏輯。

  我将通過一個例子來介紹它:

<!-- 用于顯示渲染後的标簽 -->
<ul id="element"></ul>

<!-- 定義模闆,将模闆内容放到一個script标簽中 -->
<script type="text/template" id="tpl">
    <% for(var i = 0; i < list.length; i++) { %>
        <% var item = list[i] %>
        <li>
            <span><%=item.firstName%> <%=item.lastName%></span>
            <span><%-item.city%></span>
        </li>
    <% } %>
</script>
<script type="text/javascript" src="underscore/underscore-min.js"></script>
<script type="text/javascript">
    // 擷取渲染元素和模闆内容
    var element = $('#element'),
        tpl = $('#tpl').html();
    
    // 建立資料, 這些資料可能是你從伺服器擷取的
    var data = {
        list: [
            {firstName: '<a href="#" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Zhang</a>', lastName: 'San', city: 'Shanghai'},
            {firstName: 'Li', lastName: 'Si', city: '<a href="#" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Beijing</a>'},
            {firstName: 'Wang', lastName: 'Wu', city: 'Guangzhou'},
            {firstName: 'Zhao', lastName: 'Liu', city: 'Shenzhen'}
        ]
    }
    
    // 解析模闆, 傳回解析後的内容
    var html = _.template(tpl, data);
    // 将解析後的内容填充到渲染元素
    element.html(html);
</script>      

  在本例中,我們将模闆内容放到一個<script>标簽中,你可能已經注意到标簽的type是text/template而不是text/javascript,因為它無法作為JavaScript腳本直接運作。

  我也建議你将模闆内容放在<script>中,因為如果你将它們寫在一個<div>或其它标簽中,它們可能會被添加到DOM樹中進行解析(即使你隐藏了這個标簽也無法避免)。

  _.template模闆函數隻能解析3種模闆标簽(這比Smarty、JSTL要簡單得多):

  <%  %>:用于包含JavaScript代碼,這些代碼将在渲染資料時被執行。

  <%= %>:用于輸出資料,可以是一個變量、某個對象的屬性、或函數調用(将輸出函數的傳回值)。

  <%- %>:用于輸出資料,同時會将資料中包含的HTML字元轉換為實體形式(例如它會将雙引号轉換為&quot;形式),用于避免XSS攻擊。

  當我們希望将資料中的HTML作為文本顯示出來時,常常會使用<%- %>标簽。

  Underscore還允許你修改這3種标簽的形式,如果我們想使用{% %}、{%= %}、{%- %}作為标簽,可以通過修改templateSettings來實作,就像這樣:

_.templateSettings = {
    evaluate : /\{%([\s\S]+?)\%\}/g,
    interpolate : /\{%=([\s\S]+?)\%\}/g,
    escape : /\{%-([\s\S]+?)%\}/g
}      

  在本例中,我們将模闆内容和需要填充的資料傳遞給template方法,它會按以下順序進行處理:

  • 将模闆内容解析為可執行的JavaScript(解析模闆标簽)
  • 通過with語句将解析後的JavaScript作用域修改為我們傳遞的資料對象,這使我們能夠直接在模闆中通過變量形式通路資料對象的屬性
  • 執行解析後的JavaScript(将資料填充到模闆)
  • 傳回執行後的結果

  我們經常會遇到一種情況:多次調用template方法将資料渲染到同一個模闆。

  假設我們有一個分頁清單,清單中的每一條資料都通過模闆渲染,當使用者進入下一頁,我們會擷取下一頁的資料并重新渲染,實際上每次渲染的模闆都是同一個,但剛才描述的template所有處理過程總會被執行。

  其實Underscore的template方法提供了一種更高效的調用方式,我們将上面代碼中的最後兩句修改為:

// 解析模闆, 傳回解析後的内容
var render = _.template(tpl);
var html = render(data);
// 将解析後的内容填充到渲染元素
element.html(html);      

  你會發現細微的差别:我們在調用template方法時隻傳遞了模闆内容,而沒有傳遞資料,此時template方法會解析模闆内容,生成解析後的可執行JavaScript代碼,并傳回一個函數,而函數體就是解析後的JavaScript,是以當我們調用該函數渲染資料時,就省去了模闆解析的動作。

  你應該将傳回的函數存儲起來(就像我将它存儲在render變量中一樣),再通過調用該函數來渲染資料,特别是在同一個模闆可能會被多次渲染的情況下,這樣做能提高執行效率(具體提升多少,應該根據你的模闆長度和複雜度而定,但無論如何,這都是一個良好的習慣)。

繼續閱讀