Mustache.js源碼分析

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.renderdebug,咱們順藤摸瓜梳理了mustache.js5個模塊(暫且稱它們爲: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 = {
    '&': '&',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '/': '&#x2F;',
    '`': '&#x60;',
    '=': '&#x3D;'
};

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的類型,可能的值有#^/&nametext,分別表示{}時,調用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不存在(nullundefined0'')或者爲空數組的時候才觸發渲染。

看看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最主要的是一個解析器和一個渲染器,以很是簡潔的方式實現了一個強大的模板引擎。

相關文章
相關標籤/搜索