1.什么是模板解析?
举个例子:Vue中的{{param}}写法其实就是模板语法的一种,将这种简便的模板语法与其对应的数据进行结合,生成浏览器可以解析的真实DOM的过程就是模板解析。
2.模板解析原理(模板引擎所做的工作)
对于简单的模板语法,完全可以使用正则+String.replace()对其进行替换操作。
例如下面的代码:
// 模板字符串
var templateStr = '我买了{{ thing }}, 我很{{ mood }}';
// 对应的数据
let data = {
thing: '手机',
mood: 'happy'
}
// 使用正则搭配replace方法对{{和}}中间的字符进行替换,替换成data[param]
let result = templateStr.replace(/\{\{( *\w+ *)\}\}/g, (captureStr, $1) => {
console.log($1);
return data[$1.trim()]
})
console.log(result); // 我买了手机, 我很happy
该方法只适用于简单的模板编译,但是对于复杂一点的模板,正则就显得无能为力,例如嵌套for循环等。
3.使用数组去存储代码块
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>性别:{{sex}}</p>
<p>年龄:{{age}}</p>
<ol>
{{#item.hobby}}
<li>{{.}}</li>
{{/item.hobby}}
</ol>
</div>
</li>
{{/arr}}
</ul>
对于上面的模板语法,我们想把它转为形如多层数组嵌套的形式。至于为什么,就要去问问MustacheJS库的作者了,我们只需站在巨人的肩膀上即可。
4.编写工具类Scanner
// Scanner.js
class Scanner {
constructor(templateStr) {
// 指针
this.pos = 0;
// 尾巴
this.tail = templateStr;
this.templateStr = templateStr
}
// 路过指定内容,无返回值
scan(tag) {
if(this.tail.indexOf(tag) == 0) {
this.pos += tag.length
}
}
// 指针进行扫描,遇见大括号时,返回遍历的内容
scanUtil(stopTag) {
let startPos = this.pos
while(!this.eos() && this.tail.indexOf(stopTag) != 0) {
this.pos++;
this.tail = this.templateStr.slice(this.pos)
}
return this.templateStr.slice(startPos, this.pos)
}
// 判断指针是否到头
eos() {
return this.pos >= this.templateStr.length
}
}
5.通过Scanner工具类将模板字符串转为二维数组
import Scanner from "./Scanner";
export default function parseTemplateToTokens(templateStr) {
let scanner = new Scanner(templateStr)
let tokens = []
let word = '';
while(!scanner.eos()) {
word = scanner.scanUtil('{{')
if(word != '') {
tokens.push(['text', word])
}
scanner.scan('{{')
word = scanner.scanUtil('}}')
if(word != '') {
if(word[0] == '#') {
tokens.push(['#', word.slice(1)])
} else if(word[0] == '/') {
tokens.push(['/', word.slice(1)])
} else {
tokens.push(['name', word])
}
}
scanner.scan('}}')
}
return tokens
}
经过第四步和第五步,token返回的格式长这样,由于以上内容与主线无关,将直接跳过不再解释
6.将tokens数组进行嵌套(重点)
// 这里定义一个方法,接受一个tokens数组,返回一个多层嵌套的tokens
export default function nestTokens(tokens) {
// 结果数组
let nestedTokens = []
// 栈,用于体现当前层级的深度,其实本质上用一个数字来代替
let sections = []
// 指针,用于指向需要插入token的层级,其实本质上完全可以用数字替代,但因为collector回归上一级的时候需要获取到上一级的对象,所以才用的数组
let collector = nestedTokens
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
switch(token[0]) {
case '#':
// 1. 首先网当前层的下标为2的数组添加该token,如果是第一次,则相当于在nestedTokens中添加
// 说明:其实无论在nestedTokens中push,还是在token[2]处push,操作是一样的,无非就是宿主不一样,这也正是collector的作用,也就是说,我们只需要关注什么时候改变collector的指向,后续push操作就无需关注。它可以根据#和/,自由地切换插入的层级
collector.push(token)
// 2. 接着入栈整个token(毋庸置疑)
sections.push(token)
// 第三步:把当前token的下标2处开发出来,置为[],方便后续进行操作,同时collector指针下移,后续所有push操作都将push入当前token[2]中
collector = token[2] = []
break;
case '/':
// 当扫描到/时,有两个任务,
// 1. 首先让sections出栈(毋庸置疑)
sections.pop()
// 2. 接着将collector指向上一层的下标为2的数组,如果没有上一层,则collector = nestedTokens
collector = sections.length != 0 ? sections[sections.length - 1][2] : nestedTokens
break;
default:
// 当不是#和/时,秩序无脑的向collector中push元素,至于collector的切换则有上面完成
collector.push(token)
}
}
return nestedTokens
}