目录
(2)将模板字符串变成tokens数组(parseTemplateToTokens.js):
(3)将零散的tokens数组嵌套起来(nestTokens.js):
注:上一基础篇我们了解到什么是模板引擎以及如何使用,这一篇我们通过尝试写一些底层源码来深入理解其工作流程(把模板字符串编译为虚拟DOM),下一篇中我们在讲解如何结合数据渲染出真实DOM。
一、正则实现简单模板数据填充:
最简单模板引擎实现机理,利用的是正则表达式中的replace()方法:
<div id="container">
</div>
</body>
<script src="../js/mustache.js"></script>
<script>
var templateStr = `
<h1>我买了一棵{{thing}},花了{{money}}元,好{{mood}}</h1>
`;
var data = {
thing:"白菜",
money:5,
mood:"激动"
};
function render(templateStr,data){
return templateStr.replace(/\{\{(\w+)\}\}/g,function (findStr,$1) {
console.log($1);
return data[$1];
})
};
var domStr = render(templateStr,data);
var dom = document.getElementById("container");
dom.innerHTML = domStr;
console.log(domStr);
</script>
代码解释:
replace()的第一个参数是正则表达式,第二个参数可以是一个函数。函数可以接受参数:第一个参数是findStr 是你查找的这部分内容 {{thing}}、{{money}}、{{mood}},第二个参数是 $1 你捕获到的内容 thing、money、mood。
但是正则只实现简单的模板引擎替换,如果遇到复杂的还是需要我们使用 mustache 来实现。
二、mustache库的实现机理:
1. 过程解析:
首先是模板字符串经过编译变为tokens,这一过程我们称为模板字符串变为虚拟DOM的过程。接着虚拟DOM结合数据进行解析成为真实DOM,这一过程我们称为虚拟DOM转换为真实DOM的过程。
(1) tokens解释:
tokens是一个JS的嵌套数组,就是模板字符串的JS表示。 tokens是“抽象语法树”、“虚拟节点”等等的开山鼻祖。
将模板字符串转换为JS的嵌套数组,tokens相当于一个二维数组里面存放的是一个一个的token。在每一个token中文字和标签(在模板字符串中没有处理意义)是由"text"表示,而带双大括号的变成name。
(2)过程图示:
当模板字符串嵌套层级更深的时候即有循环的存在,它将编译为嵌套更深的tokens。
可以看到 tokens[1]中有数组的嵌套。
(3)mustache库底层重点要做两个事情:
- 将模板字符串编译为tokens形式(将真实DOM变为虚拟DOM)。
注:虚拟DOM变为真实DOM的内容是在diff算法中涵盖的。(后边的博客我们有讲解到)
- 将tokens结合数据,解析为dom字符串。
三、实现底层原理:
1、代码目录:
底层实现的环境是webpack、ES6。
2、实现模板字符串编译为tokens的过程:
(1) 将模板字符串提取出来(Scanner.js):
创建一个Scanner.js实现对,模板字符串的扫描过程。Scanner.js里面有两个函数一个是scan方法,一个是scanUntil方法。
scan:方法功能弱,就是走过指定内容(大括号),没有返回值。即跳过大括号
//功能弱,就是走过指定内容,没有返回值
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
// tag有多长,比如{{长度是2,就让指针后移多少位
this.pos += tag.length;
// 尾巴也要变
this.tail = this.templateStr.substring(this.pos);
}
}
scanUntil:让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字。即找到大括号并将大括号之前和之后的文字收集并返回。
//让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
scanUntil(stopTag) {
//记录一下执行本方法的时候pos的值
const pos_backUp = this.pos;
//当尾巴的开头不是它 (定义一个尾巴当做收集当前指针所在位置到字符串最后的字符)
while (this.tail.indexOf(stopTag) != 0 && this.pos < templateStr.length) {
this.pos++;
//改变尾巴 从当前指针这个字符开始 到最后的全部字符
this.tail = this.templateStr.substr(this.pos);
}
return this.templateStr.substring(pos_backUp, this.pos);
}
Scanner.js
export default class Scanner {
constructor(templateStr) {
console.log(templateStr);
//将模板字符串写在实例上
this.templateStr = templateStr;
// 指针
this.pos = 0;
// 尾巴,一开始就是模板字符串的原文
this.tail = templateStr;
}
//功能弱,就是走过指定内容,没有返回值
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
// tag有多长,比如{{长度是2,就让指针后移多少位
this.pos += tag.length;
// 尾巴也要变
this.tail = this.templateStr.substring(this.pos);
}
}
//让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
scanUntil(stopTag) {
//记录一下执行本方法的时候pos的值
const pos_backUp = this.pos;
//当尾巴的开头不是它 (定义一个尾巴当做收集当前指针所在位置到字符串最后的字符)
while (this.tail.indexOf(stopTag) != 0 && this.pos < templateStr.length) {
this.pos++;
//改变尾巴 从当前指针这个字符开始 到最后的全部字符
this.tail = this.templateStr.substr(this.pos);
}
return this.templateStr.substring(pos_backUp, this.pos);
}
}
过程解析:
我们首先定义一个头指针pos和一个尾巴指针tail,头指针为0,尾巴指针刚开始为整个模板字符串。
scanUntil的方法就是返回"{{" 和"}}"之间的字符串,而scan方法就是跳过大括号"{{" " }}"。
scanUntil方法中我们用一个pos_backUp来记录头指针然后在while循环里面不断地改变pos的值,尾巴指针也在跟着改变,判断的条件是尾巴指针的第一个字符不是stopTag,并且pos指针小于模板字符串的长度。
scan方法则是根据tag改变pos指针,使pos指针加上tag的长度同时尾巴也跟着改变,跳过"{{"和"}}"。
(2)将模板字符串变成tokens数组(parseTemplateToTokens.js):
在parseTemplateToTokens.js中我们创建一个扫描器,在while循环中我们不断地收集scanUntil经过的字符并存起来和跳过scan经过的字符。重点在与tokens二维数组的形成。
import Scanner from './Scanner.js'
import nestTokens from "./nestTokens"
/*
*将模板字符串变为tokens数组
* */
export default function parseTemplateToTokens(templateStr) {
var tokens = [];
// 创建扫描器
var scanner = new Scanner(templateStr);
var words;
// 让扫描器工作
while (scanner.pos != templateStr.length) {
//收集
words = scanner.scanUntil("{{");
//存起来
if (words != "") {
tokens.push(['text', words]);
}
//过双大括号
scanner.scan("{{");
//收集之前出现的文字
words = scanner.scanUntil("}}");
//存起来
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("}}");
}
console.log(tokens);
return nestTokens(tokens);
}
这段代码是判断 {{ xxxx}}双大括号中间的内容,判断一下首字符。需要把“#”和“/”当做tokens数组里面子数组的第一个下标为0的元素。
//存起来
if (words != "") {
//判断一下首字符
if (words[0] == '#') {
//存起来,从下标为1的项开始存,因为下标为0的项是#
tokens.push(['#', words.substring(1)]);
} else if (words[0] == '/') {
//存起来,从下标为1的项开始存,因为下标为0的项是/
tokens.push(['/', words.substring(1)]);
} else {
// 存起来
tokens.push(['name', words]);
}
}
此时打印出来的内容相当于如下:(#和/之间的内容并没有嵌套起来)
(3)将零散的tokens数组嵌套起来(nestTokens.js):
就是把#和/之间的内容用数组装起来,这里我们运用的是栈的思想(先进后出/后进先出)我们遇见#就压栈,遇见/就出栈,#和/之间的内容都是其子元素。
新建一个nestTokens.js进行进栈压栈的操作:
定义结果数组:nestedTokens,收集器:collector,接着遍历传进来的tokens通过switch判断token的第0项是什么(有三种情况一种是“#”,一种是“/”,一种什么也不是)。
/*
* 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的选项
* */
export default function nestTokens(tokens) {
// 结果数组
var nestedTokens = [];
//栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)
//的tokens数组当前操作的这个tokens小数组
var sections = [];
//收集器指向结果数组,引用类型值,所以指向的是同一个数组
var collector = nestedTokens;
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case '#':
// 将收集器放入到这个token
collector.push(token);
// 压栈从队尾进
sections.push(token);
// 更新收集器的状态,指向数组中的嵌套数组
collector = token[2] = [];
break;
case '/':
//出栈,pop()会返回刚刚弹出的项
sections.pop();
// 改变收集器为栈结构的上一层(队尾(队尾是栈顶))
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
default:
collector.push(token);
}
}
return nestedTokens;
}
- 在switch中case为"#"时,我们将这个token数组放入到收集器中并且进行压栈,重要是更改收集器的状态。
- case为"/"时首先要出栈并改变收集器的状态(此时要判断sections数组的长度)
- 一般情况则是正常压栈
重点在于:收集器(collector)是引用类型值,它和结果数组的内存地址是一样的,所以改变收集器的值,结果数组的值会改变。
经过上面的步骤我们可以看到,已经成功将模板字符串变为虚拟DOM也就是完成了一半,下一篇中我们将会讲解如何结合数据渲染出真实DOM。
注:代码地址。