Underscore中提供了_.template函數實現模板引擎功能,它能夠將JSON數據源中的數據對應的填充到提供的字符串中去,相似於服務端渲染的模板引擎。接下來看一下Underscore是如何實現模板引擎的。javascript
工具準備
首先是_.template函數的配置項,Underscore源碼中配置了默認的配置項:java
_.templateSettings = { // 執行JavaScript語句,並將結果插入。 evaluate: /<%([\s\S]+?)%>/g, // 插入變量的值。 interpolate: /<%=([\s\S]+?)%>/g, // 插入變量的值,並進行HTML轉義。 escape: /<%-([\s\S]+?)%>/g };
每一項的意思都寫在了註釋中,修改不一樣項的正則表達式,能夠修改你傳入的字符串模板中的佔位符。默認的佔位符:git
- <% %> : 表示執行JavaScript語句。
- <%= %> : 表示插入變量的值。
- <%- %> : 表示對插入值進行HTML轉義後再插入。
源碼中還寫了一個不可能匹配的正則表達式:github
// 一個不可能有匹配項的正則表達式。
var noMatch = /(.)^/;
一個JSON(類字典),用於映射轉義字符到轉義後的字符:正則表達式
var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' };
以及一個匹配轉義字符的正則表達式和一個轉義函數。express
// 匹配須要轉義字符的正則表達式。 var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; // 返回字符對應的轉義後的字符。 var escapeChar = function (match) { return '\\' + escapes[match]; };
接下來會使用到這些變量。api
實現_.template
實現原理大體以下:瀏覽器
- 使用正則匹配傳入字符串中的全部佔位符,並讀取佔位符中的變量名或JavaScript語句。
- 構造一個字符串,用於定義渲染函數,把讀取到的變量名或JavaScript語句嵌入到字符串中,使得在使用渲染函數時,變量會被具體的值替代。若是變量名所表明的值須要轉義,則還需使用
_.escape
函數對其進行轉義,一樣寫入字符串中。 - 經過步驟二構造的字符串構造渲染函數,定義一個閉包,閉包負責調用這個渲染函數,返回閉包便可。
如何匹配傳入字符串中的佔位符是一個問題,由於一個字符串中可能包含多種或者多個佔位符,這裏用了String.prototype.replace
方法的一種不經常使用的方法。閉包
一般,咱們至少用String.prototype.replace
的簡單用法,即第一個參數爲要替換的字符串,第二個參數爲用於替換它的新字符串,該函數返回替換結果,不改變原字符串。若是要將字符串中的指定字符串所有替換,那麼第一個參數應該傳入正則表達式,而且採用全局匹配模式g
。不少人不知道String.prototype.replace
還有更加靈活的第三種用法,即第二個參數傳遞爲一個函數,這個函數的返回結果做爲替代指定字符串的新字符串,且至少接收一個參數:app
- 若是對於第一個參數沒有匹配結果,那麼回調函數只接受一個參數,即爲原字符串。
- 若是有匹配結果,那麼接收至少三個參數,依次爲匹配到的字符串、匹配字符串的開始索引和原字符串。
能夠打開瀏覽器輸入以下代碼回車進行驗證:
let str = 'abc'; str.replace(/a/g, function() { console.log(arguments); });
若是有多個匹配結果,那麼回調函數會被調用屢次:
let str = 'abcabc'; str.replace(/a/g, function() { console.log(arguments); });
回車以後,在控制檯能夠看到兩次打印結果。那麼就至關因而進行了一個循環操做,這個循環會遍歷匹配到的每一項,這樣就能夠對於匹配到的佔位符進行適當的操做了。此外,String.prototype.replace
函數還有一個很優秀的特性,若是第一個參數傳遞爲正則表達式而且含有多個捕獲組(及括號),那麼每一個捕獲組所捕獲的字符串都會做爲參數傳遞給回調函數,因此說回調函數至少接收一個參數。其參數個數能夠取決於正則表達式中的捕獲組個數。驗證如下代碼:
let str = 'abcabcabc'; str.replace(/(ab)|(c)/g, function() { console.log(arguments); });
能夠發現回調所接受的參數個數即爲3 + 正則中的捕獲組個數
。基於這個特性,Underscore做者對字符串進行了很好的處理。
實現代碼以下:
_.template = function (text, settings, oldSettings) { // 若是第二個參數爲null或undefined。。等,那麼使用oldSettings做爲settings。 if (!settings && oldSettings) settings = oldSettings; // 若是三個參數齊整,那麼使用整合後的對象做爲settings。 settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. // 匹配佔位符的正則表達式,將配置項中的三個正則合併,每個正則都是一個捕獲組,若是配置項沒有包含的話,就默認不匹配任何值。 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; var source = "__p+='"; // function回調做爲string.replace的第二個參數會傳遞至少三個參數,若是有多餘捕獲的話,也會被做爲參數依次傳入。 // string.replace只會返回替換以後的字符串,可是不會對原字符串進行修改,下面的操做實際上沒有修改text,只是借用string.replace的回調函數完成新函數的構建。 text.replace(matcher, function (match, escape, interpolate, evaluate, offset) { // 截取沒有佔位符的字符片斷,而且轉義其中須要轉義的字符。 source += text.slice(index, offset).replace(escapeRegExp, escapeChar); // 跳過佔位符,爲下一次截取作準備。 index = offset + match.length; // 轉義符的位置使用匹配到的佔位符中的變量的值替代,構造一個函數的內容。 if (escape) { // 不爲空時將轉義後的字符串附加到source。 source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } else if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { // 因爲是直接執行語句,因此直接把evaluate字符串添加到構造函數的字符串中去就好。 source += "';\n" + evaluate + "\n__p+='"; } // Adobe VMs need the match returned to produce the correct offset. // 正常來講沒有修改原字符串text,因此不返回值沒有關係,可是這裏返回了原匹配項, // 根據註釋的意思,多是爲了防止特殊環境下可以有一個正常的offset偏移量。 return match; }); source += "';\n"; // source拼湊出了一個函數定義的全部內容,爲後面使用Function構造函數作準備。 // If a variable is not specified, place data values in local scope. // 指定做用域,以取得傳入對象數據的全部屬性。 if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n'; var render; try { // 經過new Function()形式構造函數對象。 // new Function(param1, ..., paramN, funcBody) render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } var template = function (data) { return render.call(this, data, _); }; // Provide the compiled source as a convenience for precompilation. var argument = settings.variable || 'obj'; // 爲template函數添加source屬性以便於進行預編譯,以便於發現不可重現的錯誤。 template.source = 'function(' + argument + '){\n' + source + '}'; return template; }; 具體註釋都已經寫在代碼中。 能夠發現,在_.template函數中,將配置項中的三個正則合併成了一個,而且每個正則都構成了一個捕獲組,這樣回調就會接受6個參數,最後一個參數被做者忽略了。在回調中,做者分別對三種匹配項進行了處理,而後拼接到了source字符串中。 構造完source字符串以後,做者就使用了new Function()的語法構造了一個render函數,經過研究source字符串能夠發現,render實際上至關於函數: function render(settings.variable || 'obj', _) { var __t, __p = '', __j = Array.prototype.join, print = function(){ __p+=__j.call(arguments,''); }; // 若是配置了variable屬性就不須要使用with塊了。 with(obj || {}) { __p += '...' + ((__t=(" + /*須要轉義的變量*/ + "))==null?'':_.escape(__t)) + ((__t=(" + /*變量*/ + "))==null?'':__t) + ... ; /*須要執行的JavaScript字符串*/; } return __p; }
構造完這個render函數,基本的工做也就完成了。
這裏比較巧妙的點在於做者經過String.prototype.replace
函數構造函數字符串,對於每個特定的模板定製了一個特定的函數,這個函數會構造一個對應於模板的字符串,將變量填充進去,因此返回的字符串即爲咱們想要的字符串。