Underscore.js template()函數全解析

模板解析引擎有很多,比如mustache.js等。在我們解析從服務端獲取的JSON數據的時候使用模板有很大的便利。下面我們分析一下Underscore.js的template()模板渲染函數。

官網API介紹


將 JavaScript 模板編譯爲可以用於頁面呈現的函數, 對於通過JSON數據源生成複雜的HTML並呈現出來的操作非常有用。 模板函數可以使用 <%= … %>插入變量, 也可以用<% … %>執行任意的 JavaScript 代碼。 如果您希望插入一個值, 並讓其進行HTML轉義,請使用<%- … %>。 當你要給模板函數賦值的時候,可以傳遞一個含有與模板對應屬性的data對象 。 如果您要寫一個一次性的, 您可以傳對象 data 作爲第二個參數給模板 template來直接呈現, 這樣頁面會立即呈現而不是返回一個模板函數. 參數 settings 是一個哈希表包含任何可以覆蓋的設置 _.templateSettings.


var compiled = _.template("hello: <%= name %>");
compiled({name: 'moe'});
=> "hello: moe"


var list = "<% _.each(people, function(name) { %> <li><%= name %></li> <% }); %>";
_.template(list, {people: ['moe', 'curly', 'larry']});
=> "<li>moe</li><li>curly</li><li>larry</li>"


var template = _.template("<b><%- value %></b>");
template({value: '<script>'});
=> "<b>&lt;script&gt;</b>"
您也可以在JavaScript代碼中使用 print. 有時候這會比使用 <%= ... %> 更方便.


var compiled = _.template("<% print('Hello ' + epithet); %>");
compiled({epithet: "stooge"});
=> "Hello stooge"
如果ERB式的分隔符您不喜歡, 您可以改變Underscore的模板設置, 使用別的符號來嵌入代碼. 定義一個 interpolate 正則表達式來逐字匹配 嵌入代碼的語句, 如果想插入轉義後的HTML代碼 則需要定義一個 escape 正則表達式來匹配, 還有一個 evaluate 正則表達式來匹配 您想要直接一次性執行程序而不需要任何返回值的語句. 您可以定義或省略這三個的任意一個. 例如, 要執行 Mustache.js 類型的模板:


_.templateSettings = {
  interpolate: /\{\{(.+?)\}\}/g
};


var template = _.template("Hello {{ name }}!");
template({name: "Mustache"});
=> "Hello Mustache!"
默認的, template 通過 with 語句 來取得 data 所有的值. 當然, 您也可以在 variable 設置裏指定一個變量名. 這樣能顯著提升模板的渲染速度.


_.template("Using 'with': <%= data.answer %>", {answer: 'no'}, {variable: 'data'});
=> "Using 'with': no"
預編譯模板對調試不可重現的錯誤很有幫助. 這是因爲預編譯的模板可以提供錯誤的代碼行號和堆棧跟蹤, 有些模板在客戶端(瀏覽器)上是不能通過編譯的 在編譯好的模板函數上, 有 source 屬性可以提供簡單的預編譯功能.


<script>
  JST.project = <%= _.template(jstText).source %>;
</script>


源碼解讀

我對源碼進行了詳細的註釋,也搞明白了其中的邏輯。原理比較簡單,大家可細讀以下代碼。
// By default, Underscore uses ERB-style template delimiters, change the
  // following template settings to use alternative delimiters.
  // Underscore默認使用標籤格式的模板分隔符,改變下面的模板設置項可以使用你自己設置的模板分隔符。
  _.templateSettings = {
    evaluate    : /<%([\s\S]+?)%>/g,
    interpolate : /<%=([\s\S]+?)%>/g,
    escape      : /<%-([\s\S]+?)%>/g
  };

  // When customizing `templateSettings`, if you don't want to define an
  // interpolation, evaluation or escaping regex, we need one that is
  // guaranteed not to match.
  // 當定製了'templateSettings'設置項後,如果你不想定義interpolation,evaluation或者escaping的正則,
  // 我們需要一個保證在某個屬性項(evaluate,interpolate,escape)沒有的情況下的正則。
  var noMatch = /(.)^/;

  // Certain characters need to be escaped so that they can be put into a string literal.
  // 對特定字符進行轉碼(前面會加上"\"),這樣放在Function的字符串字面量的函數體中才能正常運行(類似於正則中我們想要對\符號的匹配一樣)
  var escapes = {
    "'":      "'",
    '\\':     '\\',
    '\r':     'r',
    '\n':     'n',
    '\u2028': 'u2028',
    '\u2029': 'u2029'
  };
  // 獲取escapes屬性部分的匹配的正則
  var escaper = /\\|'|\r|\n|\u2028|\u2029/g;

  var escapeChar = function(match) {
    return '\\' + escapes[match];
  };

  // JavaScript micro-templating, similar to John Resig's implementation.
  // Underscore templating handles arbitrary delimiters, preserves whitespace,
  // and correctly escapes quotes within interpolated code.
  // NB: `oldSettings` only exists for backwards compatibility.
  // JavaScript mini模板引擎,類似於John Resig的實現。Underscore的模板可以處理任意的定界符,保留空格,並且可以在插入的代碼里正確的轉義引號。
  // 注意:'oldSetting'的存在只是爲了向後兼容。
  // Underscore模板解析流程:
  // 1、準備要對整個字符串進行匹配的正則表達式;
  // 2、組裝要執行的函數體主要部分(source變量,通過對整個模板進行正則匹配來實現);
  // 3、組裝整個函數體執行部分;
  // 4、使用Function實例化出一個生成最終字符串的函數(對該函數傳入要渲染的參數即可獲得最終渲染字符串);
  // 5、提供預編譯的source參數,方便調試與錯誤追蹤
  _.template = function(text, settings, oldSettings) {
    if (!settings && oldSettings) settings = oldSettings;
	// 使用defaults方法來給settings參數賦默認值(如果evaluate、interpolate、escape任一屬性有值則不做覆蓋)
    settings = _.defaults({}, settings, _.templateSettings);
	
    // Combine delimiters into one regular expression via alternation.
	// 將界定符組合成一個正則
	// 用戶如果沒有設置界定符則以下正則是:/<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$/g
    var matcher = RegExp([
      (settings.escape || noMatch).source,
      (settings.interpolate || noMatch).source,
      (settings.evaluate || noMatch).source
    ].join('|') + '|$', 'g');

    // Compile the template source, escaping string literals appropriately.
    var index = 0;
	// 記錄編譯成的函數字符串,可通過_.template(tpl).source獲取
    var source = "__p+='";
	/**
		replace()函數的各項參數意義:
		1、第一個參數爲每次匹配的全文本($&)。
		2、中間參數爲子表達式匹配字符串,也就是括號中的東西,個數不限
		3、倒數第二個參數爲匹配文本字符串的匹配下標位置。
		4、最後一個參數表示字符串本身。
	*/
    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
	  // 將要編譯的模板中正則匹配到非分解符部分的加到source上面去,這裏做了字符串轉義處理
      source += text.slice(index, offset).replace(escaper, escapeChar);
	  // 將index跳至當前匹配分解符的結束的地方
      index = offset + match.length;
	  // 界定符內匹配到的內容(TODO:進一步解釋)
      if (escape) {
		// 需要轉碼的字符串部分的處理
        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
      } else if (interpolate) {
		// 對象屬性部分的處理
        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
      } else if (evaluate) {
		// 代碼執行部分的處理
        source += "';\n" + evaluate + "\n__p+='";
      }

      // Adobe VMs need the match returned to produce the correct offest.
	  // 將匹配到的內容原樣返回(Adobe VMs需要返回match來使得offset能夠正常,一般網頁並不需要)
      return match;
    });
    source += "';\n";
	

    // If a variable is not specified, place data values in local scope.
	// 如果沒有在第二個參數裏指定variable變量,那麼將數據值置於局部變量中執行
    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
	
	// 將組裝好的source重新組裝,成爲真正可以執行的js代碼字符串。(print相當於等號,但是比=號要方便)
	// Array.prototype.join.call(arguments,'');是將所有的參數(如果是對象則調用toString()方法轉化爲字符串)以''合併在一起
    source = "var __t,__p='',__j=Array.prototype.join," +
      "print=function(){__p+=__j.call(arguments,'');};\n" +
      source + 'return __p;\n';
	  
	// 防止在沒有傳settings.variable作爲with的作用域的時候,render函數的第一個參數名字爲obj(此時render函數格式:function(obj,_) {source}),
	// obj爲在沒有傳遞setting.variable的時候source代碼的作用域
    try {
	  // underscore的根對象也作爲一個變量傳入了函數
	  // Function傳參:前面是執行函數時的參數,最後是執行函數體字符串字面量
      var render = new Function(settings.variable || 'obj', '_', source);
    } catch (e) {
      e.source = source;
      throw e;
    }
	// 傳進去的data相當於obj
    var template = function(data) {
	  // this一般都是指向window
      return render.call(this, data, _);
    };

    // Provide the compiled source as a convenience for precompilation.
	// 提供編譯的source,方便預編譯(據官方文檔,這麼做可以對錯誤進行跟蹤定位)
    var argument = settings.variable || 'obj';
    template.source = 'function(' + argument + '){\n' + source + '}';
	// 將函數返回(對函數傳入要渲染的數據即可獲得最終渲染字符串)
    return template;
  };


發佈了45 篇原創文章 · 獲贊 10 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章