mustache.js
是一個弱邏輯的模板引擎,語法十分簡單,使用很方便。源碼(v2.2.1
)只有600+
行,且代碼結構清晰。javascript
通常來講,mustache.js
使用方法以下:html
var template = 'Hello, {{name}}'; var rendered = Mustache.render(template, { name: 'World' }); document.getElementById('container').innerHTML = rendered;
經過使用Chrome
對上述Mustache.render
的debug
,咱們順藤摸瓜梳理了mustache.js
5個模塊(暫且稱它們爲:Utils
, Scanner
, Parser
, Writer
,Context
)間的關係圖以下:java
代碼層面,Mustache.render()
方法是mustache.js
向外暴露的方法之一,正則表達式
mustache.render = function render(template, view, partials) { // 容錯處理 if (typeof template !== 'string') { throw new TypeError('Invalid template! Template should be a "string" ' + 'but "' + typeStr(template) + '" was given as the first ' + 'argument for mustache#render(template, view, partials)'); } // 調用Writer.render return defaultWriter.render(template, view, partials); };
在其內部,它首先調用了Writer.render()
方法,數組
Writer.prototype.render = function render(template, view, partials) { // 調用Writer構造器的parse方法 var tokens = this.parse(template); // 渲染邏輯,後文會分析 var context = (view instanceof Context) ? view : new Context(view); return this.renderTokens(tokens, context, partials, template); };
而Writer.render()
方法首先調用了Writer.parse()
方法,數據結構
Writer.prototype.parse = function parse(template, tags) { var cache = this.cache; var tokens = cache[template]; if (tokens == null) // 調用parseTemplate方法 tokens = cache[template] = parseTemplate(template, tags); return tokens; };
Writer.parse()
方法調用了parseTemplate
方法,
因此,歸根結底,Mustache.render()
方法首先調用parseTemplate
方法對html
字符串進行解析,
而後,將一個對象渲染到解析出來的模板中去。curl
因此,咱們得研究源碼核心所在——parseTemplate
方法。在此以前,咱們的先看一些前置方法:工具方法和掃描器。ide
Utils
)// 判斷某個值是否爲數組 var objectToString = Object.prototype.toString; var isArray = Array.isArray || function isArrayPolyfill(object) { return objectToString.call(object) === '[object Array]'; }; // 判斷某個值是否爲函數 function isFunction(object) { return typeof object === 'function'; } // 更精確的返回數組類型的typeof值爲'array',而非默認的'object' function typeStr(obj) { return isArray(obj) ? 'array' : typeof obj; } // 轉義正則表達式裏的特殊字符 function escapeRegExp(string) { return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); } // 判斷對象是否有某屬性 function hasProperty(obj, propName) { return obj != null && typeof obj === 'object' && (propName in obj); } // 正則驗證,防止Linux和Windows下不一樣spidermonkey版本致使的bug var regExpTest = RegExp.prototype.test; function testRegExp(re, string) { return regExpTest.call(re, string); } // 是不是空格 var nonSpaceRe = /\S/; function isWhitespace(string) { return !testRegExp(nonSpaceRe, string); } // 將特殊字符轉爲轉義字符 var entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=' }; function escapeHtml(string) { return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) { return entityMap[s]; }); } var whiteRe = /\s*/; // 匹配0個以上的空格 var spaceRe = /\s+/; // 匹配1個以上的空格 var equalsRe = /\s*=/; // 匹配0個以上的空格加等號 var curlyRe = /\s*\}/; // 匹配0個以上的空格加} var tagRe = /#|\^|\/|>|\{|&|=|!/; // 匹配#,^,/,>,{,&,=,!
Scanner
)// Scanner構造器,用於掃描模板 function Scanner(string) { this.string = string; // 模板總字符串 this.tail = string; // 模板剩餘待掃描字符串 this.pos = 0; // 掃描索引,即表示當前掃描到第幾個字符串 } // 若是模板掃描完成,返回true Scanner.prototype.eos = function eos() { return this.tail === ''; }; // 掃描的下一批的字符串是否匹配re正則,若是不匹配或者match的index不爲0 Scanner.prototype.scan = function scan(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 scanUntil(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; };
總的來講,掃描器,就是用來掃描字符串的。掃描器中只有三個方法:函數
eos
: 判斷當前掃描剩餘字符串是否爲空,也就是用於判斷是否掃描完了scan
: 僅掃描當前掃描索引的下一堆匹配正則的字符串,同時更新掃描索引scanUntil
: 掃描到匹配正則爲止,同時更新掃描索引如今進入parseTemplate
方法。工具
Parser
)解析器是整個源碼中最重要的方法,用於解析模板,將html
標籤與模板標籤分離。
整個解析原理爲:遍歷字符串,經過正則以及掃描器,將普通html
和模板標籤掃描而且分離,並保存爲數組tokens
。
function parseTemplate(template, tags) { if (!template) return []; var sections = []; // 用於臨時保存解析後的模板標籤對象 var tokens = []; // 保存全部解析後的對象 var spaces = []; // 包括空格對象在tokens裏的索引 var hasTag = false; // 當前行是否有{{tag}} 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(tagsToCompile) { if (typeof tagsToCompile === 'string') tagsToCompile = tagsToCompile.split(spaceRe, 2); if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) throw new Error('Invalid tags: ' + tagsToCompile); openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); } compileTags(tags || mustache.tags); var scanner = new Scanner(template); var start, type, value, chr, token, openSection; while (!scanner.eos()) { start = scanner.pos; // 開始掃描模板,掃描至{{時中止掃描,而且將此前掃描過的字符保存爲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}}與其對應 openSection = sections.pop(); // 檢查模板標籤是否閉合,{{#}}是否與{{/}}對應,即臨時保存在sections最後的{{#tag}} if (!openSection) throw new Error('Unopened section "' + value + '" at ' + start); // 是否跟當前掃描到的{{/tag}}的tagName相同 if (openSection[1] !== value) throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); // 具體原理:掃描第一個tag,sections爲[{{#tag}}], // 掃描第二個後sections爲[{{#tag}}, {{#tag2}}], // 以此類推掃描多個開始tag後,sections爲[{{#tag}}, {{#tag2}} ... {{#tag}}] // 因此接下來若是掃描到{{/tag}}則需跟sections的最後一個相對應才能算標籤閉合。 // 同時比較後還需將sections的最後一個刪除,才能進行下一輪比較。 } else if (type === 'name' || type === '{' || type === '&') { // 若是標籤類型爲name、{或&,不用清空上一行的空格 nonSpace = true; } else if (type === '=') { // 編譯標籤,爲下一次循環作準備 compileTags(value); } } // 確保sections中沒有開始標籤 openSection = sections.pop(); if (openSection) throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); return nestTokens(squashTokens(tokens)); }
咱們來看通過解析器解析以後獲得的tokens
的數據結構:
每個子項都相似下面這種結構
token[0]
爲token
的類型,可能的值有#
、^
、/
、&
、name
、text
,分別表示{}時,調用renderSection
方法
Writer.prototype.renderSection = function renderSection(token, context, partials, originalTemplate) { var self = this; var buffer = ''; // 獲取{{#xx}}中xx在傳進來的對象裏的值 var value = context.lookup(token[1]); function subRender(template) { return self.render(template, context, partials); } if (!value) return; if (isArray(value)) { // 若是爲數組,說明要複寫html,經過遞歸,獲取數組裏的渲染結果 for (var j = 0, valueLength = value.length; j < valueLength; ++j) { buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); } } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { // 若是value爲對象或字符串或數字,則不用循環,根據value進入下一次遞歸 buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); } else if (isFunction(value)) { if (typeof originalTemplate !== 'string') throw new Error('Cannot use higher-order sections without the original template'); // 若是value是方法,則執行該方法,而且將返回值保存 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); } return buffer; };
當模板標籤類型爲時,說明要當value
不存在(null
、undefined
、0
、''
)或者爲空數組的時候才觸發渲染。
看看renderInverted
方法的實現
Writer.prototype.renderInverted = function renderInverted(token, context, partials, originalTemplate) { var value = context.lookup(token[1]); // 值爲null,undefined,0,''或空數組 // 直接進入下次遞歸 if (!value || (isArray(value) && value.length === 0)) { return this.renderTokens(token[4], context, partials, originalTemplate); } };
到這爲止,mustache.js
的源碼解析完了,能夠看出來,mustache.js
最主要的是一個解析器和一個渲染器,以很是簡潔的方式實現了一個強大的模板引擎。