mustache.js
个人练习结果仓库(持续更新):Vue源码解析
可以参考之前的笔记Webpack笔记
安装:npm i -D webpack webpack-cli webpack-dev-server
webpack.config.js
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
,更方便地使用指令
编写示例代码
src \ index.js
import { mytest } from './test.js'
mytest()
src \ test.js
export const mytest = () => {
console.log('1+1=2')
}
www \ index.html
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>Documenttitle>
head>
<body>
<h2>testh2>
<script src="/virtual/bundle.js">script>
body>
html>
npm run dev
,到http://localhost:8080/查看
Scanner类功能:将模板字符串根据指定字符串(如{{
和}}
)切成多部分
有两个主要方法scan和scanUtil
先来一下构造函数
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾巴,用于获取除指定符号外的内容(即`{{`和`}}`)
this.tail = this.templateStr
}
// 让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
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(tag) {
if (this.tail.indexOf(tag) === 0) {
this.pos += tag.length
this.tail = this.templateStr.substring(this.pos)
// console.log(this.tail)
}
}
因为模板字符串中需要反复使用scan和scanUtil方法去把模板字符串完全切成多部份,所以需要循环,而循环结束的条件就是已经遍历完模板字符串了
// end of string:判断模板字符串是否已经走到尽头了
eos() {
return this.pos === this.templateStr.length
}
/*
* 扫描器类
*/
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
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
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>Documenttitle>
head>
<body>
<h2>我是{{name}}, 年龄为{{age}}岁h2>
<script src="/virtual/bundle.js">script>
<script>
const templateStr = `
我是{{name}}, 年龄为{{age}}岁
`
const data = {
name: 'clz',
age: 21
}
const domStr = TemplateEngine.render(templateStr, data)
script>
body>
html>
首先,把src / index.js
的代码修改一下,封装成parseTemplateToTokens
方法
src \ index.js
import parseTemplateToTokens from './parseTemplateToTokens.js'
window.TemplateEngine = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)
console.log(tokens)
}
}
// 把模板字符串编译成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
}
用上一个版本的试一下,嵌套数组
const templateStr = `
{{#arr}}
-
{{name}}喜欢的颜色是:
{{#colors}}
- {{.}}
{{/colors}}
{{/arr}}
`
发现存在点问题,所以需要提取特殊符号#
和/
取到words时,判断一下第一位符号是不是特殊字符,对特殊字符进行提取
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部分存好
}
}
又发现,还是没有实现,框框部分应该是tokens里的嵌套tokens才对
关键:定义一个收集器collector
,一开始指向要返回的nestTokens
数组,每当遇到#
,则把它指向新的位置,遇到/
,时,又回到上一阶,且数组是引用变量,所以给colleator
push
数据时,对应指向的位置也会跟着增加数据。
为了实现收集器colleator
能顺利回到上一阶,那么就需要增加一个栈sections
,每当遇到#
时,token入栈;而当遇到/
时,出栈,并判断sections
是否为空,为空的话,则重新指向nestTokens
,不空的话,则指向栈顶
下标为2的元素。
src \ nestTokens.js
// 把#和/之间的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)
。
直接遍历tokens数组,如果遍历的元素的第一个标记是text
,则直接与要返回的字符串相加,如果是name
,则需要数据data
中把对应属性加入到要返回的字符串中。
src \ renderTemplate.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 += data[token[1]]
}
}
return result
}
src \ index.js
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)
}
}
快成功了,开心
问题:当数据中有对象类型的数据时,会出问题。
如
const templateStr = `
我是{{name}}, 年龄为{{age}}岁, 工资为{{job.salary}}元
`
const data = {
name: 'clz',
age: 21,
job: {
type: 'programmer',
salary: 1
}
}
为什么会出现这个问题呢?
我们再看一下上面的代码
if (token[0] === 'text') {
result += token[1]
} else if (token[0] === 'name') {
result += data[token[1]]
}
把出问题的部分代进去,
result += data['job.salary']
但是这样是不行的,JavaScript不支持对象使用数组形式时,下标为x.y
的形式
那么该怎么办呢?
其实只需要把obj[x.y]
的形式变为obj[x][y]
的形式即可
src \ lookup.js
// 把` 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
}
再优化一下,如果keysStr
没有.
的话,那么可以直接返回
// 把` 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
}
数据以及模板字符串
const templateStr = `
{{#arr}}
-
{{name}}喜欢的颜色是:
{{#colors}}
- {{name}}
{{/colors}}
{{/arr}}
`
const data = {
arr: [
{
name: 'clz',
colors: [{
name: 'red',
}, {
name: 'blue'
}, {
name: 'purple'
}]
},
{
name: 'cc',
colors: [{
name: 'red',
}, {
name: 'blue'
}, {
name: 'purple'
}]
}
]
}
src \ renderTemplate(增加实现嵌套数组版本)
// 将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
}
实现简单数组的那个.
,因为数据中没有属性.
,所以需要把该属性给加上
下面的代码只拿了改的一小段
src \ renderTemplate(增加实现嵌套数组版本)
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
// 把` 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
}
成功。
最后把它挂到DOM树上
const domStr = TemplateEngine.render(templateStr, data)
document.getElementsByClassName('container')[0].innerHTML = domStr
学习视频:【尚硅谷】Vue源码解析之mustache模板引擎_哔哩哔哩_bilibili