Mustache.js源码剖析

mustache.js是一个弱逻辑的模板引擎,语法异常简朴,运用很轻易。源码(v2.2.1)只要600+行,且代码组织清楚。

一般来说,mustache.js运用要领以下:

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)间的关联图以下:

《Mustache.js源码剖析》

代码层面,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字符串举行剖析,
然后,将一个对象衬着到剖析出来的模板中去。

所以,我们得研讨源码中心地点——parseTemplate要领。在此之前,我们的先看一些前置要领:东西要领和扫描器。

东西要领(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的数据组织:

《Mustache.js源码剖析》

每个子项都相似下面这类组织

《Mustache.js源码剖析》

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最主要的是一个剖析器和一个衬着器,以异常简约的体式格局完成了一个壮大的模板引擎。

    原文作者:ybwdaisy
    原文地址: https://segmentfault.com/a/1190000018040389
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞