一、mustache 核心原理
mustache
库的机理,模版字符串编译为 tokens
,数据结合 tokens
解析为 dom
字符串。tokens
是一个JS
的嵌套数组,就是模板字符串的JS
表示。它是“抽象语法树”、“虚拟节点”等等的开山鼻祖。- 循环情况下的
tokens
,当模板字符串中有循环存在时,它将被编译为嵌套更深的tokens
。 - 双重循环情况下的
tokens
,当循环是双重的,那么tokens
会更深一层。 mustache
库底层重点要做两个事情,如下所示:
- 将模板字符串编译为
tokens
形式 - 将
tokens
结合数据,解析为dom
字符串
二、mustache 原理实现
Scanner
,扫描器类,如下所示:
constructor
时将模板字符串写到实例身上,指定指针,尾巴,一开始就是模板字符串原文。scan
时功能弱,就是走过指定内容,没有返回值。tag
有多长,比如{{
长度是2,就让指针后移多少位。尾巴也要变,改变尾巴为从当前指针这个字符开始,到最后的全部字符。scanUtil
时让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字。记录一下执行本方法的时候pos
的值。当尾巴的开头不是stopTag
的时候,就说明还没有扫描到stopTag
,写&&
很有必要,因为防止找不到,那么寻找到最后也要停止下来。改变尾巴为从当前指针这个字符开始,到最后的全部字符。eos
时指针是否已经到头,返回布尔值。
Scanner
,代码如下所示:
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr;
this.pos = 0;
this.tail = templateStr;
}
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
this.pos += tag.length;
this.tail = this.templateStr.substring(this.pos);
}
}
scanUtil(stopTag) {
const pos_backup = this.pos;
while (!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++;
this.tail = this.templateStr.substring(this.pos);
}
return this.templateStr.substring(pos_backup, this.pos);
}
eos() {
return this.pos >= this.templateStr.length;
}
};
parseTemplateToTokens
,将模板字符串变为tokens
数组,如下所示:
- 创建扫描器,让扫描器工作,收集开始标记出现之前的文字。尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格。标签中的空格不能去掉,比如
<div class="box">
不能去掉class
前面的空格。空白字符串,判断是否在标签里。如果这项不是空格,拼接上。如果这项是空格,只有当它在标签内的时候,才拼接上。存起来,去掉空格。过双大括号。 - 收集开始标记出现之前的文字。这个
words
就是{{}}
中间的东西。判断一下首字符。存起来,从下标为1的项开始存,因为下标为0的项是#
。存起来,从下标为1的项开始存,因为下标为0的项是/
。存起来。过双大括号。 - 返回折叠收集的
tokens
。
parseTemplateToTokens
,代码如下所示:
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';
export default function parseTemplateToTokens(templateStr) {
var tokens = [];
var scanner = new Scanner(templateStr);
var words;
while (!scanner.eos()) {
words = scanner.scanUtil('{{');
if (words != '') {
let isInJJH = false;
var _words = '';
for (let i = 0; i < words.length; i++) {
if (words[i] == '<') {
isInJJH = true;
} else if (words[i] == '>') {
isInJJH = false;
}
if (!/\s/.test(words[i])) {
_words += words[i];
} else {
if (isInJJH) {
_words += ' ';
}
}
}
tokens.push(['text', _words]);
}
scanner.scan('{{');
words = scanner.scanUtil('}}');
if (words != '') {
if (words[0] == '#') {
tokens.push(['#', words.substring(1)]);
} else if (words[0] == '/') {
tokens.push(['/', words.substring(1)]);
} else {
tokens.push(['name', words]);
}
}
scanner.scan('}}');
}
return nestTokens(tokens);
}
nestTokens
,函数的功能是折叠tokens
,将#
和/
之间的tokens
能够整合起来,作为它的下标为3的项,如下所示:
- 结果数组,栈结构,存放小
tokens
,栈顶(靠近端口的,最新进入的)的tokens
数组中当前操作的这个tokens
小数组。 - 收集器,天生指向
nestedTokens
结果数组,引用类型值,所以指向的是同一个数组。收集器的指向会变化,当遇见#
的时候,收集器会指向这个token
的下标为2的新数组。 - 收集器中放入这个
token
,入栈,收集器要换人。给token
添加下标为2的项,并且让收集器指向它。 - 出栈。
pop()
会返回刚刚弹出的项。改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组。 - 甭管当前的
collector
是谁,可能是结果nestedTokens
,也可能是某个token
的下标为2的数组,甭管是谁,推入collctor
即可。
nestTokens
,代码如下所示:
export default function nestTokens(tokens) {
var nestedTokens = [];
var sections = [];
var collector = nestedTokens;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case '#':
collector.push(token);
sections.push(token);
collector = token[2] = [];
break;
case '/':
sections.pop();
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
};
lookup
,功能是可以在dataObj
对象中,寻找用连续点符号的keyName
属性,如下所示:
- 看看
keyName
中有没有点符号,但是不能是.本身。如果有点符号,那么拆开。 - 设置一个临时变量,这个临时变量用于周转,一层一层找下去。每找一层,就把它设置为新的临时变量。
- 如果这里面没有点符号,就返回。
lookup
,代码如下所示:
export default function lookup(dataObj, keyName) {
if (keyName.indexOf('.') != -1 && keyName != '.') {
var keys = keyName.split('.');
var temp = dataObj;
for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]];
}
return temp;
}
return dataObj[keyName];
};
parseArray
,处理数组,结合renderTemplate
实现递归,如下所示:
- 得到整体数据
data
中这个数组要使用的部分,结果字符串。 - 遍历
v
数组,v
一定是数组。注意,下面这个循环可能是整个包中最难思考的一个循环。 - 它是遍历数据,而不是遍历
tokens
。数组中的数据有几条,就要遍历几条。这里要补一个“.”
属性,然后拼接。
parseArray
,代码如下所示:
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';
export default function parseArray(token, data) {
var v = lookup(data, token[1]);
var resultStr = '';
for(let i = 0 ; i < v.length; i++) {
resultStr += renderTemplate(token[2], {
...v[i],
'.': v[i]
});
}
return resultStr;
};
renderTemplate
,函数的功能是让tokens
数组变为dom
字符串,如下所示:
- 结果字符串,遍历
tokens
,看类型,拼起来。 - 如果是
name
类型,那么就直接使用它的值,当然要用lookup
。因为防止这里是“a.b.c”
有逗号的形式。
renderTemplate
,代码如下所示:
import lookup from './lookup.js';
import parseArray from './parseArray.js';
export default function renderTemplate(tokens, data) {
var resultStr = '';
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
if (token[0] == 'text') {
resultStr += token[1];
} else if (token[0] == 'name') {
resultStr += lookup(data, token[1]);
} else if (token[0] == '#') {
resultStr += parseArray(token, data);
}
}
return resultStr;
}
index
,全局提供SSG_TemplateEngine
对象,如下所示:
- 渲染方法,调用
parseTemplateToTokens
函数,让模板字符串能够变为tokens
数组。 - 调用
renderTemplate
函数,让tokens
数组变为dom
字符串。
index
,代码如下所示:
import parseTemplateToTokens from './parseTemplateToTokens.js';
import renderTemplate from './renderTemplate.js';
window.SSG_TemplateEngine = {
render(templateStr, data) {
var tokens = parseTemplateToTokens(templateStr);
var domStr = renderTemplate(tokens, data);
return domStr;
}
};