Vue.js Mustache模板引擎原理

什么是模板引擎?

模板引擎是将数据渲染到视图上最优雅的解决方法。

现在有一个对象数组,我们要把它渲染成一个列表:
1
2
3
4
5
var arr = [
{ name: "小红", "age": "11", sex: "女" },
{ name: "小黄", "age": "12", sex: "男" },
{ name: "小明", "age": "13", sex: "男" }
];

目标效果:

在模板引擎之前有这几种方法:

  • DOM法:很麻烦,没有实用价值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <body>
    <ul id="list"></ul>
    <script>
    var arr = [
    { name: "小红", "age": "11", sex: "女" },
    { name: "小黄", "age": "12", sex: "男" },
    { name: "小明", "age": "13", sex: "男" }
    ];

    var list = document.getElementById("list");

    for (var i = 0; i < arr.length; i++) {
    var li = document.createElement("li");
    var hdDiv = document.createElement("div");
    hdDiv.className = "hd";
    hdDiv.innerHTML = arr[i].name + '的基本信息';
    var bdDiv=document.createElement("div");
    bdDiv.className="bd";
    var p1=document.createElement("p");
    p1.innerHTML="姓名:"+arr[i].name;
    var p2=document.createElement("p");
    p2.innerHTML="年龄:"+arr[i].age;
    var p3=document.createElement("p");
    p3.innerHTML="性别:"+arr[i].sex;
    bdDiv.appendChild(p1);
    bdDiv.appendChild(p2);
    bdDiv.appendChild(p3);
    li.appendChild(hdDiv);
    li.appendChild(bdDiv);
    list.appendChild(li);
    }
    </script>
    </body>
  • 数组join法:代码比DOM法清晰,运用了数组的join方法,提高了DOM的可读性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <body>
    <ul id="list"></ul>
    <script>
    var arr = [
    { name: "小红", "age": "11", sex: "女" },
    { name: "小黄", "age": "12", sex: "男" },
    { name: "小明", "age": "13", sex: "男" }
    ];

    var list = document.getElementById("list");

    for (var i = 0; i < arr.length; i++) {
    list.innerHTML += [
    '<li>',
    ' <div class="hd">'+ arr[i].name+'的基本信息</div>',
    ' <div class="bd">',
    ' <p>姓名:'+ arr[i].name+'</p>',
    ' <p>年龄:'+ arr[i].age+'</p>',
    ' <p>性别:'+ arr[i].sex+'</p>',
    ' </div>',
    '</li>'
    ].join("");
    }
    </script>
    </body>
  • ES6模板字符串法,代码更加清晰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <body>
    <ul id="list"></ul>
    <script>
    var arr = [
    { name: "小红", "age": "11", sex: "女" },
    { name: "小黄", "age": "12", sex: "男" },
    { name: "小明", "age": "13", sex: "男" }
    ];

    var list = document.getElementById("list");

    for (var i = 0; i < arr.length; i++) {
    list.innerHTML += `
    <li>
    <div class="hd">${arr[i].name}的基本信息</div>
    <div class="bd">
    <p>姓名:${arr[i].name}</p>
    <p>年龄:${arr[i].age}</p>
    <p>性别:${arr[i].sex}</p>
    </div>
    </li>`
    }
    </script>
    </body>

可以看到虽然代码变得越来越简单可读,但是在每一次渲染的时候都需要对要渲染的数据进行循环,写法不够优雅。下面是实现同样的效果,用Mustache是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import Mustache from './lib/mustache.js';
var data = {
arr: [
{ name: "小红", "age": "11", sex: "女" },
{ name: "小黄", "age": "12", sex: "男" },
{ name: "小明", "age": "13", sex: "男" }
]
};
var list = document.getElementById("list");
var template = `
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<p>性别:{{sex}}</p>
</div>
</li>
{{/arr}}
</ul>
`;
var domStr = Mustache.render(template, data);
var container = document.querySelector(".container");
container.innerHTML = domStr;

可以看出使用Mustache写法干净了很多,不用再对数据进行for循环,而是在模板中使用{{#arr}}{{/arr}}来表示循环,再通过Mustache.render(template, data)一条语句即可生成填充好数据的HTML语句。

Mustache基本语法

渲染基础数据

1
2
3
4
5
6
7
8
9
10
11
12
import Mustache from './lib/mustache.js'

var list = document.getElementById("list");
var data = {
value: 666
};
var template = `
<div>{{value}}</div>
`;
var domStr = Mustache.render(template, data);
var container = document.querySelector(".container");
container.innerHTML = domStr;

渲染普通数组

在模板中使用.代表数组中的每一个元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<body>
<div class="container"></div>

<script type="text/template">
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</script>

<script type="module">
import Mustache from './lib/mustache.js';

var list = document.getElementById("list");

var data = {
arr: ['a', 'b', 'c']
};

var template = document.querySelector('script[type="text/template"]').innerHTML;

var domStr = Mustache.render(template, data);
var container = document.querySelector(".container");
container.innerHTML = domStr;
</script>
</body>

渲染布尔值

这里就有点像v-if的用法,当booltrue,里面的li才会显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<body>
<div class="container"></div>

<script type="text/template">
<ul>
{{#bool}}
<li>true</li>
{{/bool}}
</ul>
</script>

<script type="module">
import Mustache from './lib/mustache.js';

var list = document.getElementById("list");

var data = {
bool: true
};

var template = document.querySelector('script[type="text/template"]').innerHTML;

var domStr = Mustache.render(template, data);
var container = document.querySelector(".container");
container.innerHTML = domStr;
</script>
</body>

我们可以把模板给写在 <script type="text/template"> 里面,这样子不会被渲染到页面中,里面的语句也不会被执行,同时也拥有编辑器的提示。类似Vue的<template>

Mustache原理

Mustache不能用正则实现!

在进行简单的数据填充时,其实可以用正则来实现。先看一下JavaScript中字符串的replace方法,平时大多数时候使用的时候,replace方法的第一个参数是要被替换的值,第二个参数是要替换成的值,其实replace方法的第二个参数可以是一个回调函数:

1
2
3
4
5
6
var template = "我叫张三,我今年18岁了。";
template.replace(/我/g, (a, b, c) => {
console.log(a, b, c);
})
// 我 0 我叫张三,我今年18岁了。
// 我 5 我叫张三,我今年18岁了。

可以看到回调函数的第一个参数是匹配的值,第二个参数是匹配的值的位置,第三个参数是原串。这个回调也可以对正则进行捕获:

1
2
3
4
5
6
7
8
var template = "<h1>我叫{{name}},今年{{age}}岁了。</h1>";

template.replace(/\{\{(\w+)\}\}/g, (findStr, $1) => {
console.log(findStr, $1);
})

// {{name}} name
// {{age}} age

这时第一个参数是匹配的值,第二个参数是捕获的值。获取到捕获的值后,就是Mustache中的键名,所以可以这样写:

1
2
3
4
5
6
7
8
9
10
11
var template = "<h1>我叫{{name}},今年{{age}}岁了。</h1>";
var data = {
name: "张三",
age: 18
};
var repalcedTemplate = template.replace(/\{\{(\w+)\}\}/g, (findStr, $1) => {
console.log($1);
return data[$1];
})
console.log(repalcedTemplate);
// <h1>我叫张三,今年18岁了。</h1>

可以看到模板已经被成功渲染出数据了。但是Mustache原理并不是这个,当遇到循环、布尔等复杂数据时,用正则就不起作用了。

Mustache的思想

Mustache的作用就是把模板字符串给转换成DOM节点,在转化的过程中,引入了tokens这一概念,用来做为中间人,Mustache先把模板字符串转化成tokens,然后将要渲染的数据与tokens结合,然后再把tokens给解析成DOM节点。

看一下tokens

打开Mustache的源码,找到parseTemplate这个函数,这个就是将模板字符串转化成tokens的函数。拉到函数的末尾,将函数的返回值打印出来。

1
2
3
var tokens = nestTokens(squashTokens(tokens));
console.log(tokens);
return tokens;

然后用上一节的例子,对模板字符串进行解析:

1
2
3
4
5
6
7
import Mustache from "./lib/mustache.js";
var template = "<h1>我叫{{name}},今年{{age}}岁了。</h1>";
var data = {
name: "张三",
age: 18
};
Mustache.render(template, data);

查看控制台,tokens被打印出来了:

1
2
3
4
5
6
7
[
["text", "<h1>我叫", 0, 6],
["name", "name", 6, 14],
["text", ",今年", 14, 17],
["name", "age", 17, 24],
["text", "岁了。</h1>", 24, 32]
]

这里的tokens是一个二维数组,子数组就是一个tokentoken分为四部分,是一个元素是token的类别,text表示纯文本,name表示一个值,就是要渲染的数值,第二个元素是这个token的值,第三个和第四个元素是token的起始坐标和结束坐标,并没有起到作用。

再看一下有循环的tokens

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/template" id="template">
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>
</script>

<body>
<script type="module">
import Mustache from "./lib/mustache.js";

var template = document.getElementById("template").innerHTML;
var data = {
arr: [1, 2, 3, 4, 5]
};
Mustache.render(template, data);
</script>
</body>
1
2
3
4
5
6
7
8
9
[
["text", "\n <ul>\n", 0, 10],
["#", "arr", 18, 26, [
["text", " <li>", 27, 43],
["name", ".", 43, 48],
["text", "</li>\n", 48, 54]
], 62],
["text", " </ul>\n", 71, 81]
]

有循环的tokens是一个多维数组,其中循环的token的类型是#,然后里面又包含了其他几个token

Scanner

接下来开始写Mustache的底层源码,想要把模板字符串变成tokens,首先要对模板字符串进行扫描,在Mustache源码中,定义了一个Scanner类,用于对模板字符串进行扫描。

Scanner类有两个重要属性,一个是pos,这个属性是指针,记录了扫描器扫描模板字符串的位置;另一个是tail,这个属性是指针之后的字符串。

Scanner有两个扫描方法,一个叫做scan,一个是scanUntil,主要用于扫描的函数是scanUntil函数,函数接收一个stopTag,以此做为pos移动的标识,当pos移动到stopTag时,停止扫描,并且返回扫描到的内容。scan函数接收stopTag,并且将pos前移stopTag的长度,没有返回值。

下面是它的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
}
}

// 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
scanUntil(stopTag) {
// 记录一下执行本方法的时候pos的值
const pos_backup = this.pos;
// 当尾巴的开头不是stopTag的时候,就说明没有扫描到stopTag,继续扫描
while (this.tail.indexOf(stopTag) !== 0 && !this.eos()) {
// 每扫描一个字符,pos就要向后移动一位
this.pos++;
// 更新尾字符串
this.tail = this.templateStr.substring(this.pos);
}
// 返回扫描到的字符串
return this.templateStr.substring(pos_backup, this.pos);
}

// 判断指针是否已经到头,end of string
eos() {
return this.pos >= this.templateStr.length;
}
}

parseTemplate

然后我们创建parseTemplate函数,parseTemplate函数中接受一个模板字符串,把它交给一个Scanner实例进行扫描,然后根据扫描结果生成tokens。函数从头开始扫描字符串,当扫描到{{`时,就停止扫描,把扫描到的内容添加到`tokens`中,做为`text`;然后跳过`{{`,继续扫描,直到扫描到`}},把扫描到的内容添加到tokens中,做为name,重复上面的步骤,直到字符串的结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Scanner from "./Scanner.js";

export default function parseTemplate(templateStr) {
let scanner = new Scanner(templateStr);
let tokens = [];
let word;
while (!scanner.eos()) {
word = scanner.scanUntil("{{");
scanner.scan("{{");
if (word != '') {
tokens.push(["text", word]);
}
word = scanner.scanUntil("}}");
scanner.scan("}}");
if (word != '') {
tokens.push(["name", word]);
}
}
return tokens;
}

这时候已经可以生成简单的token了。新建一个全局对象TemplateEngine来测试一下这个函数,这个其实就是我们写的Mustache,包含一个函数render,用来将数组渲染到模板字符串中,先在render函数中调用parseTemplate函数,测试一下:

1
2
3
4
5
6
7
8
9
import parseTemplate from './parseTemplate.js';

var TemplateEngine = {
render: function (templateStr, data) {
console.log(parseTemplate(templateStr));
}
}

export default TemplateEngine;
1
2
3
4
5
6
7
8
import TemplateEngine from "../src/index.js";
window.TemplateEngine = TemplateEngine;
var template = "我叫{{name}},今年{{age}}岁";
var data = {
name: "张三",
age: 18
};
TemplateEngine.render(template, data);

可以看到这时打印出来的tokens:

1
2
3
4
5
6
7
[
["text","我叫"],
["name","name"],
["text",",今年"],
["name","age"],
["text","岁"]
]

这时的tokens是正确的,但这只是简单使用场景,如果模板字符串中有嵌套,这时就行不通了:

1
2
3
4
5
6
var template = `  
<ul>
{{#arr}}
<li>{{.}}</li>
{{/arr}}
</ul>`;
1
2
3
4
5
6
7
8
9
[
["text"," \n <ul>\n "],
["name","#arr"],
["text","\n <li>"],
["name","."],
["text","</li>\n "],
["name","/arr"],
["text","\n </ul>"]
]

可以看到循环的部分没有被正确转换,因此还需要改写一下parseTemplate函数,当函数获取到{{}}里的内容时,要对内容的首字符进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function parseTemplate(templateStr) {
let scanner = new Scanner(templateStr);
let tokens = [];
let word;
while (!scanner.eos()) {
word = scanner.scanUntil("{{");
scanner.scan("{{");
if (word != '') {
tokens.push(["text", word]);
}
word = scanner.scanUntil("}}");
scanner.scan("}}");
if (word != '') {
if (word[0] == "#") {
tokens.push(['#', word.substring(1)]);
} else if (word[0] == "/") {
tokens.push(['/', word.substring(1)]);
} else {
tokens.push(["name", word]);
}
}
}
return tokens;
}

现在tokens里的循环token并不完全,但是可以看出tokens循环的变量了:

1
2
3
4
5
6
7
8
9
[
["text"," \n <ul>\n "],
["#","arr"],
["text","\n <li>"],
["name","."],
["text","</li>\n "],
["/","arr"],
["text","\n </ul>"]
]

接下来就是要把零散的循环token嵌套起来。

nestTokens

nestTokens函数的作用是把零散的token嵌套起来,在nestTokens中,我们遍历parseTemplate生成的token数组,然后判断每一个token的第一个元素,如果第一个元素是#或者/,那就要对该token进行特殊的处理,如果是普通的token,就不做处理。

函数中引入了一个栈(sections),一个收集器(collector),用来收集tokencollector是一个可变的数组,收集tokens直到循环结束,然后把收集到的tokens放到栈顶的数组中。

collector初始是指向nestedTokens的,它运用到的JavaScript的引用类型的特性,在下面代码中,collectornestedTokens指向的是同一个数组,往collector里插入元素,就相当于在nestedToken里插入元素。

1
2
var nestedTokens = [];
var collector = nestedTokens;

当循环只是循环到普通token时,直接往collectorpush这个token

当循环遇到#时,也是往collectorpush这个token,并且也要向sectionspush这个token,用来记录当前循环的层数,同时还要把collector指向当前token的坐标为2的元素,也就是一个数组,用来收集这个循环token的子项。

当循环遇到/时,sections栈顶的元素出栈,然后collector指向section栈顶的token的坐标为2的元素,如果sections为空,那就把collector指向nestedTokens

下面是具体的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export default function nestTokens(tokens) {

// 结果数组
var nestedTokens = [];

// 栈结构,用于存放tokens
var sections = [];

// 收集器,收集tokens,直到遇到结束标签,然后把收集到的tokens放到栈顶的数组中
var collector = nestedTokens;

for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case "#":
// 收集器收集token
collector.push(token);
// 栈收集token,用来记录嵌套的层级
sections.push(token);
// 把收集器指向当前token[2],用于收集循环的token,进入下一层
// sections的栈顶的token[2]就是collector
collector = token[2] = [];
break;
case "/":
sections.pop();
// 收集器指向sections的栈顶,用于收集结束标签后的token,如果sections为空,collector指向nestedTokens
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}

将数据插入进tokens

上面我们已经完成了将模板字符串转变为token的过程了,接下来我们要完成的就是将数据插入到tokens中。思路就是遍历tokens,然后根据每一个token的类型来判断对该token的操作:如果token的类型是text,那么结果字符串就直接追加这个字符串;如果是name,那么就从data中查找这个数据,并追加它的值;如果是#,那就要对这个token进行一个递归的操作。

首先要解决当token类型为name时的问题,现在有一个对象:

1
2
3
4
5
6
7
var data = {
a:{
b:{
c:666
}
}
};

我们要获取a.b.c的值,如果我们直接用data["a.b.c"]来获取,是获取不到的,因为JavaScript没有解析.运算符的能力,所以我们要创建一个函数来解析a.b.c这样带点的路径字符串,我们把它叫做lookup函数,lookup函数接受一个数据对象和一个键名,先要判断键名中有没有.,并且键名不是.,然后对键名以.进行分割,然后对分割结果遍历,类似与数据响应式原理中的parsePath函数:

1
2
3
4
5
6
7
8
9
10
11
12
export default function lookup(data, key) {
if (key.indexOf('.') != -1 && key != '.') {
console.log(key);
let keys = key.split('.');
let result = data;
for (let i = 0; i < keys.length; i++) {
result = result[keys[i]];
}
return result;
}
return data[key];
}

然后我们可以写renderTemplate函数,用于生成DOM字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import parseArray from "./parseArray.js";
import lookup from "./lookup.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;
}

这里需要注意的是循环的情况,需要新建一个parseArray函数对循环token进行处理,parseArray函数接受当前的循环tokendata对象,然后通过lookup函数获取到被循环的数组,接着循环该数组,因为数组有多少个元素,就要生成多少个DOM字符串,然后在循环中递归调用renderTemplate函数生成DOM字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import renderTemplate from "./renderTemplate.js";
import lookup from "./lookup.js";

export default function parseArray(token, data) {
var resultStr = "";
// 获取被循环的数组
var v = lookup(data, token[1]);
for (let i = 0; i < v.length; i++) {
resultStr += renderTemplate(token[2], {
...v[i],
".": v[i]
});
}

return resultStr;
}

至此模板引擎已经完全开发完毕了。

总结

Mustache的原理其实并不难,关键是要理解其中模板字符串->tokens->DOM的思想。

我们写一个实例看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<body>
<div class="container"></div>

<script type="text/template">
<ul>
{{#arr}}
<li>{{name}}的基本信息:
<p>性别:{{sex}}</p>
<p>年龄:{{age}}</p>
<p>爱好:
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</p>
</li>
{{/arr}}
</ul>
</script>

<script type="module">
import TemplateEngine from "../src/index.js";

var template = document.querySelector("script[type='text/template']").innerHTML;
var data = {
arr: [{
name: "张三",
age: 18,
sex: "男",
hobbies: ["篮球", "足球"]
}, {
name: "李四",
age: 19,
sex: "女",
hobbies: ["篮球", "羽毛球"]
}, {
name: "王五",
age: 20,
sex: "男",
hobbies: ["足球", "羽毛球"]
}],
a: {
b: {
c: 666
}
}
};

var domStr = TemplateEngine.render(template, data);
var containerDiv = document.querySelector(".container");
containerDiv.innerHTML += domStr
</script>
</body>

这时已经可以正确生成DOM字符串了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<ul>
<li>张三的基本信息:
<p>性别:男</p>
<p>年龄:18</p>
<p>爱好:
<ol>
<li>篮球</li>
<li>足球</li>
</ol>
</p>
</li>
<li>李四的基本信息:
<p>性别:女</p>
<p>年龄:19</p>
<p>爱好:
<ol>
<li>篮球</li>
<li>羽毛球</li>
</ol>
</p>
</li>
<li>王五的基本信息:
<p>性别:男</p>
<p>年龄:20</p>
<p>爱好:
<ol>
<li>足球</li>

<li>羽毛球</li>
</ol>
</p>
</li>
</ul>

代码

  • index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import parseTemplate from './parseTemplate.js';
    import renderTemplate from './renderTemplate.js';

    var TemplateEngine = {
    render: function (templateStr, data) {
    var tokens = parseTemplate(templateStr);
    var domStr = renderTemplate(tokens, data);
    return domStr;
    }
    };

    export default TemplateEngine;
  • lookup.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 用于获取数据

    export default function lookup(data, key) {
    if (key.indexOf('.') != -1 && key != '.') {
    console.log(key);
    let keys = key.split('.');
    let result = data;
    for (let i = 0; i < keys.length; i++) {
    result = result[keys[i]];
    }
    return result;
    }
    return data[key];
    }
  • nestTokens.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    export default function nestTokens(tokens) {

    // 结果数组
    var nestedTokens = [];

    // 栈结构,用于存放tokens
    var sections = [];

    // 收集器,收集tokens,直到遇到结束标签,然后把收集到的tokens放到栈顶的数组中
    var collector = nestedTokens;

    for (let i = 0; i < tokens.length; i++) {
    let token = tokens[i];
    switch (token[0]) {
    case "#":
    // 收集器收集token
    collector.push(token);
    // 栈收集token,用来记录嵌套的层级
    sections.push(token);
    // 把收集器指向当前token[2],用于收集循环的token,进入下一层
    // sections的栈顶的token[2]就是collector
    collector = token[2] = [];
    break;
    case "/":
    sections.pop();
    // 收集器指向sections的栈顶,用于收集结束标签后的token,如果sections为空,collector指向nestedTokens
    collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
    break;
    default:
    collector.push(token);
    }
    }
    return nestedTokens;
    }
  • parseArray.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import renderTemplate from "./renderTemplate.js";
    import lookup from "./lookup.js";

    export default function parseArray(token, data) {
    var resultStr = "";
    // 获取被循环的数组
    var v = lookup(data, token[1]);
    for (let i = 0; i < v.length; i++) {
    resultStr += renderTemplate(token[2], {
    ...v[i],
    ".": v[i]
    });
    }

    return resultStr;
    }
  • parseTemplate.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    import Scanner from "./Scanner.js";
    import nestTokens from "./nestedTokens.js";

    export default function parseTemplate(templateStr) {
    let scanner = new Scanner(templateStr);
    let tokens = [];
    let word;
    while (!scanner.eos()) {
    word = scanner.scanUntil("{{");
    scanner.scan("{{");
    if (word != '') {
    tokens.push(["text", word]);
    }
    word = scanner.scanUntil("}}");
    scanner.scan("}}");
    if (word != '') {
    if (word[0] == "#") {
    tokens.push(['#', word.substring(1)]);
    } else if (word[0] == "/") {
    tokens.push(['/', word.substring(1)]);
    } else {
    tokens.push(["name", word]);
    }
    }
    }
    return nestTokens(tokens);
    }
  • renderTemplate.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import parseArray from "./parseArray.js";
    import lookup from "./lookup.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;
    }
  • Scanner.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    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);
    }
    }

    // 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUntil(stopTag) {
    // 记录一下执行本方法的时候pos的值
    const pos_backup = this.pos;
    // 当尾巴的开头不是stopTag的时候,就说明没有扫描到stopTag,继续扫描
    while (this.tail.indexOf(stopTag) !== 0 && !this.eos()) {
    // 每扫描一个字符,pos就要向后移动一位
    this.pos++;
    // 更新尾字符串
    this.tail = this.templateStr.substring(this.pos);
    }
    // 返回扫描到的字符串
    return this.templateStr.substring(pos_backup, this.pos);
    }

    // 判断指针是否已经到头,end of string
    eos() {
    return this.pos >= this.templateStr.length;
    }
    }