理解Underscore中的_.template函數

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函數構造函數字符串,對於每個特定的模板定製了一個特定的函數,這個函數會構造一個對應於模板的字符串,將變量填充進去,因此返回的字符串即爲咱們想要的字符串。

相關文章
相關標籤/搜索