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', publicPath: "/virtual/" },
devServer: { static: path.join(__dirname, 'www'), compress: false, port: 8080, } }
|
修改 package.json
,更方便地使用指令
编写示例代码
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/查看
实现Scanner类
Scanner类功能:将模板字符串根据指定字符串(如 {{` 和` }}
)切成多部分
有两个主要方法scan和scanUtil
- scan: 跳过指定内容,无返回值
- scanUtil:让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
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) } }
|
eos方法
因为模板字符串中需要反复使用scan和scanUtil方法去把模板字符串完全切成多部份,所以需要循环,而循环结束的条件就是已经遍历完模板字符串了
1 2 3 4
| 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) } }
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) }
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>
|
封装并实现将模板字符串编译成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
| 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]) }
scanner.scan('{{')
words = scanner.scanUtil('}}') if (words !== '') { tokens.push(['name', words]) }
scanner.scan('}}') }
return tokens }
|
提取特殊符号
用上一个版本的试一下,嵌套数组
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> `
|
发现存在点问题,所以需要提取特殊符号 #
和 /
取到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]) } }
|
又发现,还是没有实现,框框部分应该是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
|
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)
。
将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) } }
|
快成功了,开心
问题:当数据中有对象类型的数据时,会出问题。
如
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 } }
|
为什么会出现这个问题呢?
我们再看一下上面的代码
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
的形式
那么该怎么办呢?
其实只需要把 obj[x.y]
的形式变为obj[x][y]
的形式即可
src \ lookup.js
1 2 3 4 5 6 7 8 9 10 11 12 13
|
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 }
|
再优化一下,如果 keysStr
没有 .
的话,那么可以直接返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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
|
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 }
|
实现简单数组的那个 .
,因为数据中没有属性 .
,所以需要把该属性给加上
下面的代码只拿了改的一小段
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] }) } }
|
但是,还是有问题
回到 lookup
中查看
微操一手:
src \ lookup.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
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 }
|
成功。
最后把它挂到DOM树上
1 2
| const domStr = TemplateEngine.render(templateStr, data) document.getElementsByClassName('container')[0].innerHTML = domStr
|
学习视频:【尚硅谷】Vue源码解析之mustache模板引擎_哔哩哔哩_bilibili