1. mustache 以前js大批量生成 html的方式
// 1. dom
for(let i = 0; i < 10 ; i++) {
const div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
}
// 2. 字符传拼接
let htmlStr = ''
for(let i = 0; i < 10; i++) {
htmlStr += '<div>' + i + '</div>'
}
document.body.innerHTML = htmlStr
// 3. join
let tagArr = []
let data = [
{
name: 'zzzz',
age: 18
},
{
name: 'xxxx',
age: 20
}
]
for(let i = 0; i < data.length; i++) {
const token = data[i]
tagArr.push(
`<div>
<strong>name: ${token['name']}</strong>
<em>age: ${token['age']}</em>
</div>`
)
}
document.body.innerHTML = tagArr.join('')
2. mustache出现
<script src="./mustache.js"></script>
<script>
const template = `
{{#people}}
<ul>
<li>
name:{{name}}
</li>
<li>
age:{{age}}
</li>
</ul>
{{/people}}
`
const view = {
people: [
{
name: 'zzzz',
age: 20
},
{
name: 'xxx',
age: 18
},
{
name: 'yyy',
age: 111
}
]
}
document.body.innerHTML = Mustache.render(template, view)
</script>
mustache 无疑是一个质的飞跃,这样的思维模式也被用在了当下流行的框架当中
那么就让我来分享我在阅读mustache的理解吧
3. 源码分析
首先我们可以发现我们之前调用的Mustache.render 实际上是调用了 defaultWriter.render, 这个时候继续网上看,我们会看到 defaultWriter = new Writer(), 同样的这个时候回到 暴露出来的 mustache对象 ,我们可以看到
mustache.escape = escapeHtml;
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
以及
var mustache = {
tags: [ '{{', '}}' ],
escape: undefined,
Scanner: undefined,
Context: undefined,
Writer: undefined
}
这时我们就可以开始解剖 mustache 的源码了
3.1 功能解析
//进入到Writer中,我们可以看到一下代码
Writer.prototype.render = function render (template, view, partials, config) {
var tags = this.getConfigTags(config); // tags 配置 用于scanner 的 标识tag 例如 {{
var tokens = this.parse(template, tags);
var context = (view instanceof Context) ? view : new Context(view, undefined);
return this.renderTokens(tokens, context, partials, template, config); // 简单的认识 只需要关注前两个参数
};
//我们着重观察 template 和 view 的去向, 发现 由 他们生成了 tokens 和 context,确实最后也是 调用了
//this.renderTokens(tokens, context) 返回了最终的结果
// 那从这里我们 就 引出了 this.parse 和 Context
3.2 this.parse 到底为何方神圣
看到下方代码,提取出我们需要的关键所在, 显然当我们第一次 解析传入的模板时是不可能存在 tokens的,那么parseTemplate又是什么呢?带着这个疑问,我们继续探索
Writer.prototype.parse = function parse (template, tags) {
//var cache = this.templateCache;
//var cacheKey = template + ':' + (tags || mustache.tags).join(':');
//var isCacheEnabled = typeof cache !== 'undefined';
var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
if (tokens == undefined) { // 保存解析 token
tokens = parseTemplate(template, tags); // parseTemplate
isCacheEnabled && cache.set(cacheKey, tokens);
}
return tokens;
};
但是当我们找到了它,我们发现这居然是一个100+行的代码块显然对于我这种源码阅历不充分的小萌新不是很友好,这个时候我们需要划分一下结构,我们看到它主要的组成应该是下面的一个结构,阅读 stripSpace函数,compileTags我们可以发现它们分别是用于去除空格,和tag解析的,默认情况 我们使用 mustache.tags = [ ‘{{’, ‘}}’ ],所以这里我们将注意点放在 scanner ,squashTokens,nestTokens上
-parseTemplate
-stripSpace
-compileTags
-scanner
-while循环
-squashTokens
-nestTokens
3.2.1 Scanner解读
//每次调用parseTemplate都会有 var scanenr = new Scanner,因此我们需要对Scanner一探究竟
/**
* A simple string scanner that is used by the template parser to find
* tokens in template strings.
* 阅读描述我们发现scanner的作用是在于将传入字符串转换为 tokens arr,接着我们去看它的具体实现
*/
function Scanner (string) {
this.string = string; //需要解析的所有字符串
this.tail = string; //需要继续解析的字符串
this.pos = 0;
}
/**
* Returns `true` if the tail is empty (end of string).
*/
// 判断扫描是否结束
Scanner.prototype.eos = function eos () {
return this.tail === '';
};
/**
* Tries to match the given regular expression at the current position.
* Returns the matched text if it can match, the empty string otherwise.
*/
//用正则匹配扫描部分
// 用于跳过 {{}} <% ... 等边界符号
Scanner.prototype.scan = function scan (re) {
var match = this.tail.match(re);
if (!match || match.index !== 0) //字符串index 0-n 与re匹配
return '';
var string = match[0];
this.tail = this.tail.substring(string.length); // 跳过扫描部分
this.pos += string.length; // 移动指针
return string;
};
/**
* Skips all text until the given regular expression can be matched. Returns
* the skipped string, which is the entire tail if no match can be made.
*/
/**
例如 "1243243453453{{name}}"
输入正则匹配 {{
那么我们需要返回1243243453453
*/
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;
};
ok, 完成了Scanner的阅读我获取到几个有用信息
Scanner.prototype.scanUntil 会在遇到匹配标签时停下并返回前面那段字符串
Scanner.prototype.scan 会在遇到匹配标签时停下,跳过并且返回该匹配项
话不多说以图示意
好的接下来让我们阅读一下,这个while循环
value = scanner.scanUntil(openingTagRe)
if (value) {
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
chr = value.charAt(i);
if (isWhitespace(chr)) {//空白符
spaces.push(tokens.length);
indentation += chr;
} else {
nonSpace = true;
lineHasNonSpace = true;
indentation += ' ';
}
tokens.push([ 'text', chr, start, start + 1 ]); //text用于标识该token的类型,区别view的需要
start += 1;
// Check for whitespace on the current line.
// 有换行符的操作 ...
}
}
/*首先我们需要明确,在 "{{" 头标签之前停下的返回的match是不需要使用我们的view进行填充的,因此可以直接向tokens数组中push
这里需要这样一个逐个字符操作的原因是为了适配更多的场景比如<tag class="xx xx">,进行字符拆解作用可以便于之后stripSpace,
squashTokens这些函数的操作
*/
if (!scanner.scan(openingTagRe)) // 跳过前标签 例如:{{,不存在前标签则退出while循环
break;
hasTag = true;
// Get the tag type.
type = scanner.scan(tagRe) || 'name'; //获取类似 {{#name}} 中的 #
scanner.scan(whiteRe); // 跳过空白符
// ......
if (type === '=') {
value = scanner.scanUntil(equalsRe);
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); // 返回 {{# name}}中的name部分
}
if (type == '>') {
token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
} else {
token = [ type, value, start, scanner.pos ];
}
在这里我们需要确认一点 text 的 token 会被拆分为单个字符,但是 其他类型 如 name类型 #类型 都不会被被拆分为单个字符
3.2.2 squashTokens解读
//squash Tokens 字面理解对tokens进行压缩,前面我们也提到text 会被拆分那么这个时候我们需要重新在进行组装
function squashTokens (tokens) {
var squashedTokens = [];
var token, lastToken; //lastToken 用于拼接,引用的思想
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
if (token) {
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { //token 此处为合并的关键句
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
squashedTokens.push(token);
lastToken = token; //保持对当前 token 的引用
}
}
}
return squashedTokens;
}
3.2.3 nestTokens解读:这里也是关键的一步
//nestTokens 字面理解 嵌套 tokens,在进行压缩后,使tokens变成一种token树的结构,为后面token tree --> htmlStr 做准备
function nestTokens (tokens) {//整体 使用 栈数据结构的思想 + 引用保存
var nestedTokens = [];
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] = []; // 更换collector的引用,token[4] 使为引用的token的4号索引位置创建一个空数组
break;
case '/':
section = sections.pop(); // 弹栈
section[5] = token[2];
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
我们需要获得一个如下结构的 token tree
tokenTree = [
['text', 'value'],
['#', 'property', childTree: [
['text', 'value'],
...
]],
['/', 'property'],
...
]
到此我们就完成了 template string -> token tree 的解析 就是 render必要操作的第一步
3.3 context 与 view
对于context,我们认为我们需要知道的是它与view 的关系
3.3.1首先是Context的属性
function Context (view, parentContext) { // Context 对应 view的管理 --> 也就是 data
this.view = view;
this.cache = { '.': this.view }; //默认 增加 { '.' : this.view }
this.parent = parentContext; //通过 this.parent 引用 将整个 view 联系起来
}
3.3.2 其次是其中的一个关键函数 lookup
这个函数也是有一定的篇幅,但是我们只需要我们所关注的
1.首先我们的得确定我们在应用中会出先 {{xxx.xxx}}的形式
2.其次mustache库中提供了一种下面的支持来更好的遍历 [1,2,3] 这种形式的数组
{{#list}}
{{.}}
{{/list}}
3.我们会先使用当前 context的 属性值,但是当前不存在的时候我们不应该立即undefined,而是向上查找 this.parent
const view = {
list: [{name: "xxx"}],
apps: [1,2,3]
}
{{#list}}
<h1>
{{#apps}}
<strong>{{name}}</strong>
{{/apps}}
</h1>
{{/list}}
//输出结果 ===>
<h1>
<strong>xxx</strong>
<strong>xxx</strong>
<strong>xxx</strong>
</h1>
3.4 最后的渲染 this.renderTokens
3.4.1 从简单的render 函数看起
在通过对this.renderTokens函数的寻找过程中
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {
var buffer = '';
var token, symbol, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
value = undefined;
token = tokens[i];
symbol = token[0]; //获取token的 type
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);
//... 中间这部分的判断并非我的关注点
else if (symbol === 'name') value = this.escapedValue(token, context, config);
else if (symbol === 'text') value = this.rawValue(token);
if (value !== undefined)
buffer += value;
}
return buffer;
};
/*
其中 this.renderSection,this.escapedValue,this.rawValue(token); 应该是大同小异, 对text需要进行转义防止对页面插入一些脚本
*/
3.4.2 this.renderSection
既然 this.renderSection,this.escapedValue,this.rawValue(token); 应该是大同小异,那么我们就取其中最为重要的 'this.renderSection’做文章,
但是终究还是要用this.escapedValue,this.rawValue进行模板的填充
//配合上述的 this.renderTokens 形成递归,可以遍历整个token tree
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {
var self = this;
var buffer = '';
var value = context.lookup(token[1]); // 取出view中 #property 对应的数据,用于判断
// This function is used to render an arbitrary template
// in the current context by higher-order sections.
function subRender (template) {
return self.render(template, context, partials, config);
}
if (!value) return;
// 判断对应value 的 type 并且 创建一个新的 context
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);
}
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);
} else if (isFunction(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, config);
}
return buffer;
};
最终this.renderTokens 返回的 buffer就是我们调用 mustache.render(template., view)的结果
4. 结束总结
第一次分享个人的源码理解可能有许多需要努力的地方,如果你看到这里,那么感谢你的浏览,如有错误希望指出
下面是我学习mustache源码的方式
如有需要以下链接自取:
我是 加油 希望成为你的朋友
以下为个人blog地址,原创内容转载前请声明来源
https://sunboyzgz.github.io/2021/01/27/hello-mustache/