mustache是一個很輕的前端模板引擎,由於以前接手的項目用了這個模板引擎,本身就也繼續用了一會以爲還不錯,最近項目相對沒那麼忙,因而就抽了點時間看了一下這個的源碼。源碼不多,也就只有六百多行,因此比較容易閱讀。作前端的話,仍是要多看優秀源碼,這個模板引擎的知名度還算挺高,因此其源碼也確定有值得一讀的地方。html
本人前端小菜,寫這篇博文純屬本身記錄一下以便作備忘,同時也想分享一下,但願對園友有幫助。若解讀中有不當之處,還望指出。前端
若是沒用過這個模板引擎,建議 去 https://github.com/janl/mustache.js/ 試着用一下,上手很容易。git
摘取部分官方demo代碼(固然還有其餘基本的list遍歷輸出): github
數據: { "name": { "first": "Michael", "last": "Jackson" }, "age": "RIP" } 模板寫法: * {{name.first}} {{name.last}} * {{age}} 渲染效果: * Michael Jackson * RIP
OK,那就開始來解讀它的源碼吧:正則表達式
首先先看下源碼中的前面多行代碼:apache
var Object_toString = Object.prototype.toString; var isArray = Array.isArray || function (object) { return Object_toString.call(object) === '[object Array]'; }; function isFunction(object) { return typeof object === 'function'; } function escapeRegExp(string) { return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); } // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 // See https://github.com/janl/mustache.js/issues/189 var RegExp_test = RegExp.prototype.test; function testRegExp(re, string) { return RegExp_test.call(re, string); } var nonSpaceRe = /\S/; function isWhitespace(string) { return !testRegExp(nonSpaceRe, string); } var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; function escapeHtml(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); } var whiteRe = /\s*/; //匹配0個或以上空格 var spaceRe = /\s+/; //匹配一個或以上空格 var equalsRe = /\s*=/; //匹配0個或者以上空格再加等於號 var curlyRe = /\s*\}/; //匹配0個或者以上空格再加}符號 var tagRe = /#|\^|\/|>|\{|&|=|!/; //匹配 #,^,/,>,{,&,=,!
這些都比較簡單,都是一些爲後面主函數準備的工具函數,包括數組
· toString和test函數的簡易封裝緩存
· 判斷對象類型的方法數據結構
· 字符過濾正則表達式關鍵符號的方法curl
· 判斷字符爲空的方法
· 轉義字符映射表 和 經過映射表將html轉碼成非html的方法
· 一些簡單的正則。
通常來講mustache在js中的使用方法都是以下:
var template = $('#template').html(); Mustache.parse(template); // optional, speeds up future uses var rendered = Mustache.render(template, {name: "Luke"}); $('#target').html(rendered);
因此,咱們接下來就看下parse的實現代碼,咱們在源碼裏搜索parse,因而找到這一段
mustache.parse = function (template, tags) { return defaultWriter.parse(template, tags); };
再經過找defaultWriter的原型Writer類後,很容易就能夠找到該方法的核心所在,就是parseTemplate方法,這是一個解析器,不過在看這個方法以前,還得先看一個類:Scanner,顧名思義,就是掃描器,源碼以下
/** * 簡單的字符串掃描器,用於掃描獲取模板中的模板標籤 */ function Scanner(string) { this.string = string; //模板總字符串 this.tail = string; //模板剩餘待掃描字符串 this.pos = 0; //掃描索引,即表示當前掃描到第幾個字符串 } /** * 若是模板被掃描完則返回true,不然返回false */ Scanner.prototype.eos = function () { return this.tail === ""; }; /** * 掃描的下一批的字符串是否匹配re正則,若是不匹配或者match的index不爲0; * 即例如:在"abc{{"中掃描{{結果能獲取到匹配,可是index爲4,因此返回"";若是在"{{abc"中掃描{{能獲取到匹配,此時index爲0,即返回{{,同時更新掃描索引 */ Scanner.prototype.scan = function (re) { var match = this.tail.match(re); if (!match || match.index !== 0) return ''; var string = match[0]; this.tail = this.tail.substring(string.length); this.pos += string.length; return string; }; /** * 掃描到符合re正則匹配的字符串爲止,將匹配以前的字符串返回,掃描索引設爲掃描到的位置 */ Scanner.prototype.scanUntil = function (re) { var index = this.tail.search(re), match; switch (index) { case -1: match = this.tail; this.tail = ""; break; case 0: match = ""; break; default: match = this.tail.substring(0, index); this.tail = this.tail.substring(index); } this.pos += match.length; return match; };
掃描器,就是用來掃描字符串,在mustache用於掃描模板代碼中的模板標籤。掃描器中就三個方法:
eos:判斷當前掃描剩餘字符串是否爲空,也就是用於判斷是否掃描完了
scan:僅掃描當前掃描索引的下一堆匹配正則的字符串,同時更新掃描索引,註釋裏我也舉了個例子
scanUntil:掃描到匹配正則爲止,同時更新掃描索引
看完掃描器,咱們再回歸一下,去看一下解析器parseTemplate方法,模板的標記標籤默認爲"{{}}",雖然也能夠本身改爲其餘,不過爲了統一,因此下文解讀的時候都默認爲{{}}:
function parseTemplate(template, tags) { if (!template) return []; var sections = []; // 用於臨時保存解析後的模板標籤對象 var tokens = []; // 保存全部解析後的對象 var spaces = []; // 保存空格對象在tokens裏的索引 var hasTag = false; var nonSpace = false; // 去除保存在tokens裏的空格標記 function stripSpace() { if (hasTag && !nonSpace) { while (spaces.length) delete tokens[spaces.pop()]; } else { spaces = []; } hasTag = false; nonSpace = false; } var openingTagRe, closingTagRe, closingCurlyRe; //將tag轉成正則,默認的tag爲{{和}},因此轉成匹配{{的正則,和匹配}}的正則,已經匹配}}}的正則(由於mustache的解析中若是是{{{}}}裏的內容則被解析爲html代碼) function compileTags(tags) { if (typeof tags === 'string') tags = tags.split(spaceRe, 2); if (!isArray(tags) || tags.length !== 2) throw new Error('Invalid tags: ' + tags); openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*'); closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1])); closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1])); } compileTags(tags || mustache.tags); var scanner = new Scanner(template); var start, type, value, chr, token, openSection; while (!scanner.eos()) { start = scanner.pos; // Match any text between tags. // 開始掃描模板,掃描至{{時中止掃描,而且將此前掃描過的字符保存爲value value = scanner.scanUntil(openingTagRe); if (value) { //遍歷{{前的字符 for (var i = 0, valueLength = value.length; i < valueLength; ++i) { chr = value.charAt(i); //若是當前字符爲空格,則用spaces數組記錄保存至tokens裏的索引 if (isWhitespace(chr)) { spaces.push(tokens.length); } else { nonSpace = true; } tokens.push([ 'text', chr, start, start + 1 ]); start += 1; // 若是遇到換行符,則將前一行的空格去掉 if (chr === '\n') stripSpace(); } } // 判斷下一個字符串中是否有{[,同時更新掃描索引至{{後一位 if (!scanner.scan(openingTagRe)) break; hasTag = true; //掃描標籤類型,是{{#}}仍是{{=}}仍是其餘 type = scanner.scan(tagRe) || 'name'; scanner.scan(whiteRe); //根據標籤類型獲取標籤裏的值,同時經過掃描器,刷新掃描索引 if (type === '=') { value = scanner.scanUntil(equalsRe); //使掃描索引更新爲\s*=後 scanner.scan(equalsRe); //使掃描索引更新爲}}後,下面同理 scanner.scanUntil(closingTagRe); } else if (type === '{') { value = scanner.scanUntil(closingCurlyRe); scanner.scan(curlyRe); scanner.scanUntil(closingTagRe); type = '&'; } else { value = scanner.scanUntil(closingTagRe); } // 匹配模板閉合標籤即}},若是沒有匹配到則拋出異常,同時更新掃描索引至}}後一位,至此時即完成了一個模板標籤{{#tag}}的掃描 if (!scanner.scan(closingTagRe)) throw new Error('Unclosed tag at ' + scanner.pos); // 將模板標籤也保存至tokens數組中 token = [ type, value, start, scanner.pos ]; tokens.push(token); //若是type爲#或者^,也將tokens保存至sections if (type === '#' || type === '^') { sections.push(token); } else if (type === '/') { //若是type爲/則說明當前掃描到的模板標籤爲{{/tag}},則判斷是否有{{#tag}}與其對應 // 檢查模板標籤是否閉合,{{#}}是否與{{/}}對應,即臨時保存在sections最後的{{#tag}},是否跟當前掃描到的{{/tag}}的tagName相同 // 具體原理:掃描第一個tag,sections爲[{{#tag}}],掃描第二個後sections爲[{{#tag}} , {{#tag2}}]以此類推掃描多個開始tag後,sections爲[{{#tag}} , {{#tag2}} ... {{#tag}}] // 因此接下來若是掃描到{{/tag}}則需跟sections的最後一個相對應才能算標籤閉合。同時比較後還需將sections的最後一個刪除,才能進行下一輪比較 openSection = sections.pop(); if (!openSection) throw new Error('Unopened section "' + value + '" at ' + start); if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); } else if (type === 'name' || type === '{' || type === '&') { nonSpace = true; } else if (type === '=') { compileTags(value); } } // 保證sections裏沒有對象,若是有對象則說明標籤未閉合 openSection = sections.pop(); if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); //在對tokens裏的數組對象進行篩選,進行數據的合併及剔除 return nestTokens(squashTokens(tokens)); }
解析器就是用於解析模板,將html標籤即內容與模板標籤分離,整個解析原理爲遍歷字符串,經過最前面的那幾個正則以及掃描器,將普通html和模板標籤{{#tagName}}{{/tagName}}{{^tagName}}掃描出來而且分離,將每個{{#XX}}、{{^XX}}、{{XX}}、{{/XX}}還有普通不含模板標籤的html等所有抽象爲數組保存至tokens。
tokens的存儲方式爲:
token[0]爲token的type,可能值爲:# ^ / & name text等分別表示{{#XX}}、{{^XX}}、{{/XX}}、{{&XX}}、{{XX}}、以及html文本等
token[1]爲token的內容,若是是模板標籤,則爲標籤名,若是爲html文本,則是html的文本內容
token[2],token[3]爲匹配開始位置和結束位置,後面將數據結構轉換成樹形結構的時候還會有token[4]和token[5]
具體的掃描方式爲以{{}}爲掃描依據,利用掃描器的scanUtil方法,掃描到{{後中止,經過scanner的scan方法匹配tagRe正則(/#|\^|\/|>|\{|&|=|!/)從而判斷出{{後的字符是否爲模板關鍵字符,再用scanUtil方法掃描至}}中止,獲取獲取到的內容,此時就能夠獲取到tokens[0]、tokens[1]、tokens[2],再調用一下scan更新掃描索引,就能夠獲取到token[3]。同理,下面的字符串也是如此掃描,直至最後一行return nestTokens(squashTokens(tokens))以前,掃描出來的結果爲,模板標籤爲一個token對象,若是是html文本,則每個字符都做爲一個token對象,包括空格字符。這些數據所有按照掃描順序保存在tokens數組裏,不只雜亂並且量大,因此最後一行代碼中的squashTokens方法和nestTokens用來進行數據篩選以及整合。
首先來看下squashTokens方法,該方法主要是整合html文本,對模板標籤的token對象沒有進行處理,代碼很簡單,就是將連續的html文本token對象整合成一個。
function squashTokens(tokens) { var squashedTokens = []; var token, lastToken; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; if (token) { if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { lastToken[1] += token[1]; lastToken[3] = token[3]; } else { squashedTokens.push(token); lastToken = token; } } } return squashedTokens; }
整合完html文本的token對象後,就經過nestTokens進行進一步的整合,遍歷tokens數組,若是當前token爲{{#XX}}或者{{^XX}}都說明是模板標籤的開頭標籤,因而把它的第四個參數做爲收集器存爲collector進行下一輪判斷,若是當前token爲{{/}}則說明遍歷到了模板閉合標籤,取出其相對應的開頭模板標籤,再給予其第五個值爲閉合標籤的開始位置。若是是其餘,則直接扔進當前的收集器中。如此遍歷完後,tokens裏的token對象就被整合成了樹形結構
function nestTokens(tokens) { var nestedTokens = []; //collector是個收集器,用於收集當前標籤子元素的工具 var collector = nestedTokens; var sections = []; var token, section; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; switch (token[0]) { case '#': case '^': collector.push(token); sections.push(token); //存放模板標籤的開頭對象 collector = token[4] = []; //此處可分解爲:token[4]=[];collector = token[4];即將collector指向當前token的第4個用於存放子對象的容器 break; case '/': section = sections.pop(); //當發現閉合對象{{/XX}}時,取出與其相對應的開頭{{#XX}}或{{^XX}} section[5] = token[2]; collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; //若是sections未遍歷完,則說明仍是有可能發現{{#XX}}開始標籤,因此將collector指向最後一個sections中的最後一個{{#XX}} break; default: collector.push(token); //若是是普通標籤,扔進當前的collector中 } } //最終返回的數組即爲樹形結構 return nestedTokens; }
通過兩個方法的篩選和整合,最終出來的數據就是精簡的樹形結構數據:
至此,整個解析器的代碼就分析完了,而後咱們來分析渲染器的代碼。
parseTemplate將模板代碼解析爲樹形結構的tokens數組,按照平時寫mustache的習慣,用完parse後,就是直接用 xx.innerHTML = Mustache.render(template , obj),由於此前會先調用parse解析,解析的時候會將解析結果緩存起來,因此當調用render的時候,就會先讀緩存,若是緩存裏沒有相關解析數據,再調用一下parse進行解析。
Writer.prototype.render = function (template, view, partials) { var tokens = this.parse(template); //將傳進來的js對象實例化成context對象 var context = (view instanceof Context) ? view : new Context(view); return this.renderTokens(tokens, context, partials, template); };
可見,進行最終解析的renderTokens函數以前,還要先把傳進來的須要渲染的對象數據進行處理一下,也就是把數據包裝成context對象。因此咱們先看下context部分的代碼:
function Context(view, parentContext) { this.view = view == null ? {} : view; this.cache = { '.': this.view }; this.parent = parentContext; } /** * 實例化一個新的context對象,傳入當前context對象成爲新生成context對象的父對象屬性parent中 */ Context.prototype.push = function (view) { return new Context(view, this); }; /** * 獲取name在js對象中的值 */ Context.prototype.lookup = function (name) { var cache = this.cache; var value; if (name in cache) { value = cache[name]; } else { var context = this, names, index; while (context) { if (name.indexOf('.') > 0) { value = context.view; names = name.split('.'); index = 0; while (value != null && index < names.length) value = value[names[index++]]; } else if (typeof context.view == 'object') { value = context.view[name]; } if (value != null) break; context = context.parent; } cache[name] = value; } if (isFunction(value)) value = value.call(this.view); console.log(value) return value; };
context部分代碼也是不多,context是專門爲樹形結構提供的工廠類,context的構造函數中,this.cache = {'.':this.view}是把須要渲染的數據緩存起來,同時在後面的lookup方法中,把須要用到的屬性值從this.view中剝離到緩存的第一層來,也就是lookup方法中的cache[name] = value,方便後期查找時先在緩存裏找
context的push方法比較簡單,就是造成樹形關係,將新的數據傳進來封裝成新的context對象,而且將新的context對象的parent值指向原來的context對象。
context的lookup方法,就是獲取name在渲染對象中的值,咱們一步一步來分析,先是判斷name是否在cache中的第一層,若是不在,才進行深度獲取。而後將進行一個while循環:
先是判斷name是否有.這個字符,若是有點的話,說明name的格式爲XXX.XX,也就是很典型的鍵值的形式。而後就將name經過.分離成一個數組names,經過while循環遍歷names數組,在須要渲染的數據中尋找以name爲鍵的值。
若是name沒有.這個字符,說明是一個單純的鍵,先判斷一下須要渲染的數據類型是否爲對象,若是是,就直接獲取name在渲染的數據裏的值。
經過兩層判斷,若是沒找到符合的值,則將當前context置爲context的父對象,再對其父對象進行尋找,直至找到value或者當前context無父對象爲止。若是找到了,將值緩存起來。
看完context類的代碼,就能夠看渲染器的代碼了:
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) { var buffer = ''; var self = this; function subRender(template) { return self.render(template, context, partials); } var token, value; for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { token = tokens[i]; switch (token[0]) { case '#': value = context.lookup(token[1]); //獲取{{#XX}}中XX在傳進來的對象裏的值 if (!value) continue; //若是不存在則跳過 //若是爲數組,說明要複寫html,經過遞歸,獲取數組裏的渲染結果 if (isArray(value)) { for (var j = 0, valueLength = value.length; j < valueLength; ++j) { //獲取經過value渲染出的html buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } } else if (typeof value === 'object' || typeof value === 'string') { //若是value爲對象,則不用循環,根據value進入下一次遞歸 buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); } else if (isFunction(value)) { //若是value是方法,則執行該方法,而且將返回值保存 if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); // Extract the portion of the original template that the section contains. value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); if (value != null) buffer += value; } else { buffer += this.renderTokens(token[4], context, partials, originalTemplate); } break; case '^': //若是爲{{^XX}},則說明要當value不存在(null、undefine、0、'')或者爲空數組的時候才觸發渲染 value = context.lookup(token[1]); // Use JavaScript's definition of falsy. Include empty arrays. // See https://github.com/janl/mustache.js/issues/186 if (!value || (isArray(value) && value.length === 0)) buffer += this.renderTokens(token[4], context, partials, originalTemplate); break; case '>': //防止對象不存在 if (!partials) continue; //>即直接讀取該值,若是partials爲方法,則執行,不然獲取以token爲鍵的值 value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; if (value != null) buffer += this.renderTokens(this.parse(value), context, partials, value); break; case '&': //若是爲&,說明該屬性下顯示爲html,經過lookup方法獲取其值,而後疊加到buffer中 value = context.lookup(token[1]); if (value != null) buffer += value; break; case 'name': //若是爲name說明爲屬性值,不做爲html顯示,經過mustache.escape即escapeHtml方法將value中的html關鍵詞轉碼 value = context.lookup(token[1]); if (value != null) buffer += mustache.escape(value); break; case 'text': //若是爲text,則爲普通html代碼,直接疊加 buffer += token[1]; break; } } return buffer; };
原理仍是比較簡單的,由於tokens的樹形結構已經造成,渲染數據就只須要按照樹形結構的順序進行遍歷輸出就好了。
不過仍是大概描述一下,buffer是用來存儲渲染後的數據,遍歷tokens數組,經過switch判斷當前token的類型:
若是是#,先獲取到{{#XX}}中的XX在渲染對象中的值value,若是沒有該值,直接跳過該次循環,若是有,則判斷value是否爲數組,若是爲數組,說明要複寫html,再遍歷value,經過遞歸獲取渲染後的html數據。若是value爲對象或者普通字符串,則不用循環輸出,直接獲取以value爲參數渲染出的html,若是value爲方法,則執行該方法,而且將返回值做爲結果疊加到buffer中。若是是^,則當value不存在或者value是數組且數組爲空的時候,才獲取渲染數據,其餘判斷都是差很少。
經過這堆判斷以及遞歸調用,就能夠把數據完成渲染出來了。
至此,Mustache的源碼也就解讀完了,Mustache的核心就是一個解析器加一個渲染器,以很是簡潔的代碼實現了一個強大的模板引擎。