Vue源码之mustache模板引擎(二) 手写实现mustache


Vue源码之mustache模板引擎(二) 手写实现mustache

mustache.js

个人练习结果仓库(持续更新):Vue源码解析

webpack配置

可以参考之前的笔记Webpack笔记

安装: npm i -D webpack webpack-cli webpack-dev-server

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path');

module.exports = {
entry: path.join(__dirname, 'src', 'index.js'),
mode: 'development',
output: {
filename: 'bundle.js',
// 虚拟打包路径,bundle.js文件没有真正的生成
publicPath: "/virtual/"
},

devServer: {
// 静态文件根目录
static: path.join(__dirname, 'www'),
// 不压缩
compress: false,
port: 8080,
}
}

修改 package.json,更方便地使用指令

image-20220313161823530


编写示例代码

src \ index.js

1
2
3
import { mytest } from './test.js'

mytest()

src \ test.js

1
2
3
export const mytest = () => {
console.log('1+1=2')
}

www \ index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h2>test</h2>
<script src="/virtual/bundle.js"></script>
</body>

</html>

** npm run dev**,到http://localhost:8080/查看

image-20220313161924306


实现Scanner类

Scanner类功能:将模板字符串根据指定字符串(如 {{` 和` }})切成多部分

有两个主要方法scanscanUtil

  • scan: 跳过指定内容,无返回值
  • scanUtil:让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符

image-20220314000348836

scanUtil方法

先来一下构造函数

1
2
3
4
5
6
7
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾巴,用于获取除指定符号外的内容(即`{{`和`}}`)
this.tail = this.templateStr
}

1
2
3
4
5
6
7
8
9
10
11
12
// 让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
scanUtil(stopTag) {
const start = this.pos // 存放开始位置,用于返回结束前遍历过的字符

// 没到指定内容时,都一直循环,尾巴也跟着变化
while (this.tail.indexOf(stopTag) !== 0 && this.pos < this.templateStr.length) { // 后面的另一个条件必须,因为最后需要跳出循环
this.pos++
this.tail = this.templateStr.substring(this.pos)
}

return this.templateStr.substring(start, this.pos) // 返回结束前遍历过的字符
}

scan方法

1
2
3
4
5
6
7
8
// 跳过指定内容,无返回值
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
this.pos += tag.length
this.tail = this.templateStr.substring(this.pos)
// console.log(this.tail)
}
}

eos方法

因为模板字符串中需要反复使用scanscanUtil方法去把模板字符串完全切成多部份,所以需要循环,而循环结束的条件就是已经遍历完模板字符串了

1
2
3
4
// end of string:判断模板字符串是否已经走到尽头了
eos() {
return this.pos === this.templateStr.length
}

完整类

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
/*
* 扫描器类
*/

export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾巴,用于获取除指定符号外的内容(即`{{`和`}}`)
this.tail = this.templateStr
}

// 跳过指定内容,无返回值
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
this.pos += tag.length
this.tail = this.templateStr.substring(this.pos)
// console.log(this.tail)
}
}

// 让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
scanUtil(stopTag) {
const start = this.pos // 存放开始位置,用于返回结束前遍历过的字符

// 没到指定内容时,都一直循环,尾巴也跟着变化
while (this.tail.indexOf(stopTag) !== 0 && this.pos < this.templateStr.length) { // 后面的另一个条件必须,因为最后需要跳出循环
this.pos++
this.tail = this.templateStr.substring(this.pos)
}

return this.templateStr.substring(start, this.pos) // 返回结束前遍历过的字符
}

// end of string:判断模板字符串是否已经走到尽头了
eos() {
return this.pos === this.templateStr.length
}
}

测试使用

src / index.js

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

window.TemplateEngine = {
render(templateStr, data) {
// 实例化一个扫描器
const scanner = new Scanner(templateStr)

while (!scanner.eos()) {
let words = scanner.scanUtil('{{')
console.log(words)
scanner.scan('{{')

words = scanner.scanUtil('}}')
console.log(words)
scanner.scan('}}')
}
}
}

www / index.html

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h2>我是{{name}}, 年龄为{{age}}岁</h2>
<script src="/virtual/bundle.js"></script>
<script>
const templateStr = `
<h2>我是{{name}}, 年龄为{{age}}</h2>
`
const data = {
name: 'clz',
age: 21
}

const domStr = TemplateEngine.render(templateStr, data)

</script>
</body>

</html>

image-20220314002049092


封装并实现将模板字符串编译成tokens数组

首先,把 src / index.js的代码修改一下,封装成 parseTemplateToTokens方法

src \ index.js

1
2
3
4
5
6
7
8
import parseTemplateToTokens from './parseTemplateToTokens.js'

window.TemplateEngine = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)
console.log(tokens)
}
}

实现简单版本

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
// 把模板字符串编译成tokens数组
import Scanner from './Scanner.js'

export default function parseTemplateToTokens() {
const tokens = []

// 实例化一个扫描器
const scanner = new Scanner(templateStr)

while (!scanner.eos()) {
let words = scanner.scanUtil('{{')
if (words !== '') {
tokens.push(['text', words]) // 把text部分存好::左括号之前的是text
}

scanner.scan('{{')

words = scanner.scanUtil('}}')
if (words !== '') {
tokens.push(['name', words]) // 把name部分存好::右括号之前的是name
}

scanner.scan('}}')
}

return tokens
}

image-20220314142032812


提取特殊符号

用上一个版本的试一下,嵌套数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const templateStr = `
<ul>
{{#arr}}
<li>
{{name}}喜欢的颜色是:
<ol>
{{#colors}}
<li>{{.}}</li>
{{/colors}}
</ol>
</li>
{{/arr}}
</ul>
`

image-20220314142439828

发现存在点问题,所以需要提取特殊符号 # /


取到words时,判断一下第一位符号是不是特殊字符,对特殊字符进行提取

1
2
3
4
5
6
7
8
9
10
11
12
if (words !== '') {
switch (words[0]) {
case '#':
tokens.push(['#', words.substring(1)])
break
case '/':
tokens.push(['/', words.substring(1)])
break
default:
tokens.push(['text', words])// 把text部分存好
}
}

image-20220315184648878

又发现,还是没有实现,框框部分应该是tokens里的嵌套tokens才对


实现嵌套tokens

关键:定义一个收集器collector ,一开始指向要返回的 nestTokens数组,每当遇到 #,则把它指向新的位置,遇到 /,时,又回到上一阶,且数组是引用变量,所以给 colleator push数据时,对应指向的位置也会跟着增加数据。

为了实现收集器 colleator能顺利回到上一阶,那么就需要增加一个栈 sections,每当遇到 #时,token入栈;而当遇到 /时,出栈,并判断 sections是否为空,为空的话,则重新指向 nestTokens,不空的话,则指向 栈顶下标为2的元素。


src \ 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
// 把#和/之间的tokens整合起来,作为#所在数组的下标为2的项

export default function nestTokens(tokens) {
const nestTokens = []
const sections = [] // 栈结构
let collector = nestTokens

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]

switch (token[0]) {
case '#':
collector.push(token)
console.log(token)
sections.push(token) // 入栈

token[2] = []
collector = token[2]
break
case '/':
sections.pop()
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens
break
default:
collector.push(token)
}
}

return nestTokens
}

另外,parseTemplateToTokens函数中返回的不再是 tokens,而是nestTokens(tokens)

image-20220315184914505


将tokens数组结合数据解析成dom字符串

实现简单版本

直接遍历tokens数组,如果遍历的元素的第一个标记是 text,则直接与要返回的字符串相加,如果是 name,则需要数据 data中把对应属性加入到要返回的字符串中。

src \ renderTemplate.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function renderTemplate(tokens, data) {
let result = ''

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]

if (token[0] === 'text') {
result += token[1]
} else if (token[0] === 'name') {
result += data[token[1]]
}
}

return result
}

src \ index.js

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

window.TemplateEngine = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)

const domStr = renderTemplate(tokens, data)
console.log(domStr)
}
}

image-20220316110816619

快成功了,开心


问题:当数据中有对象类型的数据时,会出问题。

1
2
3
4
5
6
7
8
9
10
11
const templateStr = `
<h2>我是{{name}}, 年龄为{{age}}岁, 工资为{{job.salary}}元</h2>
`
const data = {
name: 'clz',
age: 21,
job: {
type: 'programmer',
salary: 1
}
}

image-20220316110854642


为什么会出现这个问题呢?

我们再看一下上面的代码

1
2
3
4
5
if (token[0] === 'text') {
result += token[1]
} else if (token[0] === 'name') {
result += data[token[1]]
}

把出问题的部分代进去,

1
result += data['job.salary']

但是这样是不行的,JavaScript不支持对象使用数组形式时,下标为 x.y的形式

image-20220316111512509

那么该怎么办呢?

其实只需要把 obj[x.y]的形式变为obj[x][y] 的形式即可

src \ lookup.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// 把` obj[x.y]`的形式变为`obj[x][y] `的形式

export default function lookup(dataObj, keysStr) {

const keys = keysStr.split('.')
let temp = dataObj

for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]]
}

return temp
}

image-20220316112721171


再优化一下,如果 keysStr没有 .的话,那么可以直接返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 把` obj[x.y]`的形式变为`obj[x][y] `的形式

export default function lookup(dataObj, keysStr) {

if (keysStr.indexOf('.') === -1) {
return dataObj[keysStr]
}


const keys = keysStr.split('.')
let temp = dataObj

for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]]
}

return temp
}

通过递归实现嵌套数组版本

数据以及模板字符串

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
const templateStr = `
<ul>
{{#arr}}
<li>
{{name}}喜欢的颜色是:
<ol>
{{#colors}}
<li>{{name}}</li>
{{/colors}}
</ol>
</li>
{{/arr}}
</ul>
`
const data = {
arr: [
{
name: 'clz',
colors: [{
name: 'red',
}, {
name: 'blue'
}, {
name: 'purple'
}]
},
{
name: 'cc',
colors: [{
name: 'red',
}, {
name: 'blue'
}, {
name: 'purple'
}]
}
]
}

src \ renderTemplate(增加实现嵌套数组版本)

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
// 将tokens数组结合数据解析成dom字符串

import lookup from './lookup.js'

export default function renderTemplate(tokens, data) {
let result = ''

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]

if (token[0] === 'text') {
result += token[1]

} else if (token[0] === 'name') {
result += lookup(data, token[1])

} else if (token[0] === '#') {
let datas = data[token[1]] // 拿到所有的数据数组

for (let i = 0; i < datas.length; i++) { // 遍历数据数组,实现循环
result += renderTemplate(token[2], datas[i]) // 递归调用
}
}
}

return result
}

image-20220316141222936


实现简单数组的那个 .,因为数据中没有属性 .,所以需要把该属性给加上

下面的代码只拿了改的一小段

src \ renderTemplate(增加实现嵌套数组版本)

1
2
3
4
5
6
7
8
9
10
 else if (token[0] === '#') {
let datas = data[token[1]] // 拿到所有的数据数组

for (let i = 0; i < datas.length; i++) { // 遍历数据数组,实现循环
result += renderTemplate(token[2], {// 递归调用
...datas[i], // 使用扩展字符串...,把对象展开,再添加.属性为对象本身
'.': datas[i]
})
}
}

但是,还是有问题

image-20220316142004937


回到 lookup中查看

image-20220316142324569

微操一手:

src \ lookup.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 把` obj[x.y]`的形式变为`obj[x][y] `的形式

export default function lookup(dataObj, keysStr) {
if (keysStr.indexOf('.') === -1 || keysStr === '.') {
return dataObj[keysStr]
}


const keys = keysStr.split('.')
let temp = dataObj

for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]]
}

return temp
}

image-20220316142456833

成功。


最后把它挂到DOM树上

1
2
const domStr = TemplateEngine.render(templateStr, data)
document.getElementsByClassName('container')[0].innerHTML = domStr

image-20220316143056922


学习视频:【尚硅谷】Vue源码解析之mustache模板引擎_哔哩哔哩_bilibili


文章作者: 赤蓝紫
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 赤蓝紫 !
评论
  目录