本文章主要讲解的是 vue 中 mustache 模板引擎的底层机理是如何实现的,主要分为四大部分:先了解
什么是模板引擎,掌握 mustache 基本使用,分析 mustache 的底层核心机理,最后手写实现 mustache库 底层代码,拒绝纸上谈兵。整个过程是一个循序渐进的过程,本文章全是满满的干货,确保你能真正的理解 Vue模板引擎 底层是如何工作的。最后希望各位大佬点个赞!!好了话不多说,开始 mustache模板引擎 之旅!!
知识储备
- 会写一些J
avaScript常见算法,比如递归、二维数组遍历等; - 熟悉
ES6的常用特性,比如let、箭头函数、解构赋值等等; - 了解
webpack和webpack-dev-server
如果对webpack不是很了解的小伙伴,可以去之前更新的webpack专栏文章 webpack入门到精通专栏 进行精细的阅读。
什么是模板引擎
模板引擎是将数据变为视图最优雅的解决方案。先将数据变成DOM,然后再将DOM结构渲染到页面当中。
// 数据
const data = {
array: [
{name: 'Alex', sex: '男', age: 18},
{name: 'Jack', sex: '男', age: 20},
{name: '青峰', sex: '男', age: 19},
]
}
<!-- DOM结构视图 -->
<ul>
<li>
<div class="hd">Alex的基本信息</div>
<div class="bd">
<p>姓名:Alex</p>
<p>性别:男</p>
<p>年龄:18</p>
</div>
</li>
<li>
<div class="hd">Jack的基本信息</div>
<div class="bd">
<p>姓名:Jack</p>
<p>性别:男</p>
<p>年龄:20</p>
</div>
</li>
<li>
<div class="hd">Alex的基本信息</div>
<div class="bd">
<p>姓名:青峰</p>
<p>性别:男</p>
<p>年龄:19</p>
</div>
</li>
</ul>
上面的例子中,在Vue中,只需要使用一个v-for就很简单的将它给遍历出来,然后直接渲染到页面上。
<li -v-for = "(item, index) in data.array" :key="index">
历史上曾经出现的数据变为视图的方法
纯DOM方法
使用 纯dOM方法 遍历数据创建 DOM标签,而且还要手动上树。
const data = {
array: [
{name: 'Alex', sex: '男', age: 18},
{name: 'Jack', sex: '男', age: 20},
{name: '青峰', sex: '男', age: 19},
]
}
let ul = document.querySelector('.list')
for(let i = 0; i < data.array.length; i++) {
let oli = document.createElement('li')
oli.innerText = data.array[i].name + '基本信息'
let divhd = document.createElement('div')
divhd.className = "hd"
let divbd = document.createElement('div')
divbd.className = "bd"
let p1 = document.createElement('p')
let p2 = document.createElement('p')
let p3 = document.createElement('p')
p1.innerText = '姓名:' + data.array[i].name
p2.innerText = '性别:' + data.array[i].sex
p3.innerText = '年龄:' + data.array[i].age
divbd.appendChild(p1)
divbd.appendChild(p2)
divbd.appendChild(p3)
oli.appendChild(divhd)
oli.appendChild(divbd)
ul.appendChild(oli)
}
看到上面创建纯DOM的方法,我相信,你们看到都已经头皮发麻了,这还不算复杂的例子,要是循环里面再嵌套循环,我想你们都在考虑转行了。这种方法非常本拙,没有一点实战价值。
数组 join()方法
因为上面纯DOM的方法非常复杂,所以前人们想到了一个使用字符串代替结构化的html来代替DOM结构,用来创建DOM标签。
const list = document.querySelector('.list')
const data = {
array: [
{name: 'Alex', sex: '男', age: 18},
{name: 'Jack', sex: '男', age: 20},
{name: '青峰', sex: '男', age: 19},
]
}
// 遍历数据 以字符串的视角将htnl字符串添加到list中
for(let i = 0; i < data.array.length; i++) {
list.innerHTML += [
'<li>',
' <div class="hd">' + data.array[i].name +'基本信息</div>',
' <div class="bd">',
' <p>姓名: '+ data.array[i].name + '</p>',
' <p>性别:'+ data.array[i].sex + '</p>',
' <p>年龄:'+ data.array[i].age + '</p>',
' </div>',
'</li>'
].join('')
}
join()方法 是不是比上面的 纯DOM方法 简单的得多,看起来代码也更加的简洁明了,符合我们编程的方式。
ES6 的反引号法
我们还可以使用 ES6 语法的反引号模板字符串的方式进行优化我们的join方法。
const list = document.querySelector('.list')
const data = {
array: [
{name: 'Alex', sex: '男', age: 18},
{name: 'Jack', sex: '男', age: 20},
{name: '青峰', sex: '男', age: 19},
]
}
for(let i = 0; i < data.array.length; i++) {
list.innerHTML += `
<li>
<div class="hd">${data.array[i].name}的基本信息</div>
<div class="bd">
<p>姓名:${data.array[i].name}</p>
<p>性别:${data.array[i].sex}</p>
<p>年龄:${data.array[i].age}</p>
</div>
</li>
`
}
上面的三种方法中,我们在实际的开发中更加常用的是第三种反引号的方式创建标签,并且 渲染DOM 到视图层上。接着我们来介绍一下最优雅的将数据变为视图的 mustache方法。
mustache 的基本使用
- 引入
mustache库,可以通过 npm 也可以去 CDN 使用 script引入。 - 使用
Mustache.render()方法将模板与数据合并,第一个参数表示模板,第二个参数表示数据。
循环对象数组
在 mustache 中可以循环时候必须要 有{{#}} 开始符号和 {{/}} 结束符号。
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
array: [
{ name: 'Alex', sex: '男', age: 18 },
{ name: 'Jack', sex: '男', age: 20 },
{ name: '青峰', sex: '男', age: 19 },
]
}
var templateStr = `
<ul>
{{#array}}
<li>
<div class="hd">{{name}}的基本信息</div>
<div class="bd">
<p>姓名:{{name}}</p>
<p>性别:{{sex}}</p>
<p>年龄:{{age}}</p>
</div>
</li>
{{/array}}
</ul>
`
let dom = Mustache.render(templateStr,data)
// 写入innerHtml中
container.innerHTML = dom
</script>
循环嵌套数组
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
array: [
{ name: 'Alex', hobies:['打篮球', '羽毛球']},
{ name: 'Jack', hobies:['游泳', '唱歌']},
{ name: '青峰', hobies:['玩游戏', '踢足球']},
]
}
var templateStr = `
<ul>
{{#array}}
<li>
{{name}}的爱好
<ol>
{{#hobies}}
<li>{{.}}</li>
{{/hobies}}
</ol>
</li>
{{/array}}
</ul>
`
let dom = Mustache.render(templateStr,data)
// 写入innerHtml中
container.innerHTML = dom
</script>
循环简单数组
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
array:['青峰', 'Alex', 'Jack']
}
var templateStr = `
{{#array}}
<ul>
<li>
{{.}}
</li>
</ul>
{{/array}}
`
let dom = Mustache.render(templateStr,data)
// 写入innerHtml中
container.innerHTML = dom
</script>
不循环
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
name: '青峰',
age: 18
}
var templateStr = `
<h1>姓名:{{name}},年龄为:{{age}}</h1>
`
let dom = Mustache.render(templateStr,data)
// 写入innerHtml中
container.innerHTML = dom
</script>
布尔值
在mustache 还可以使用条件判断渲染的功能,这一点跟我们使用的 vue 的 v-if 非常相似。但是需要注意:不能写表达式,这也验证了 mustache 是一种弱类型的库。
<div class="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
const container = document.querySelector('.container')
const data = {
isShow: true
}
var templateStr = `
{{#isShow}}
<h3>青峰</h3>
{{/isShow}}
`
let dom = Mustache.render(templateStr,data)
// 写入innerHtml中
container.innerHTML = dom
</script>
以上就是mustache的基本用法,我们发现,除了不循环,其他的用法需要使用 {{#}} 开始符号和 {{/}} 结束符号。
mustache 底层的核心机理
当我们一开始使用 mustache 的时候,可能很多小伙伴以为 mustache库 就是使用正则表达式的思路来实现的。当然,有这种想法是不错的,确实是可实现一些简单的例子,但是,在一些复杂的情况下,正则表达式的思路就不行了,比如循环嵌套就不能实现。
当然我们也要试一下能否用正则表达式来实现。
replace()方法
在实现之前,我们先要了解一下正则表达式中的 replace()方法 是如何使用的,可以让我们更好的理解我们所编写的代码是什么含义。
使用 replace()方法 用来匹配正则表达式:
- 第一个参数:
要匹配的正则, - 第二个参数:
可以是替换的字符串,也可以是一个函数。 - 第二个参数是函数的形式:第一个参数表示
匹配到的正则部分,第二个参数表示匹配到的字符,第三个表示匹配到的字符所在位置,第四个表示需要匹配的字符串。一般来说第二个参数是我们想要的数据。
<div class="container"></div>
<script>
const container = document.querySelector('.container')
const data = {
name: '青峰',
age: 18
}
var templateStr = `
<h3>姓名:{{name}},年龄:{{age}}</h3>
`
// 正则里面()表示捕获里面的字母数据
let dom = templateStr.replace(/\{\{(\w+)\}\}/g, (...args) => data[args[1]])
console.log(dom);
</script>
一个最简单的模板引擎的实现机理就写好了,利用的是正则表达式中的
replace()方法。replace()方法的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,再结合data对象,即可进行智能的替换。
底层 tokens 思想
通过上面,我们知道mustache不是用正则表达式来实现的,那么它底层是如何实现的呢?其实它实现的机理就是下面的那张图。
我们解读一下上面图片的基本流程:
要将模板字符串先编译成 tokens,tokens当作中间的过渡形态,然后再将数据与tokens结合,并解析成DOM字符串。
那么问题来了?tokens是什么呢?其实tokens是一个JS的嵌套数组,说白了就是模板字符串的JS表示,它是 AST(抽象语法树)、虚拟节点的起源。看着概念可能比较抽象,我们用代码的形式来理解什么是tokens。
简单的 tokens
模板字符串:
<h1>姓名:{{name}},年龄:{{age}}</h1>
tokens:
[
["text", "<h1>姓名:"],
["name", "name"],
["text", ",年龄:"],
["name", "age"],
["tetx", "</h1>"]
]
循环嵌套的 tokens
模板字符串:
var templateStr = `
<ul>
{{#array}}
<li>
{{name}}的爱好
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/array}}
</ul>
`
tokens:
[
["text", "<ul>"],
["#", "array",[
["text", "<li>"],
["name", "name"],
["text", "的爱好<ol>],
["#", "hobbies",[
["text", "<li>"],
["name", "."],
["text", "</li>"]
]],
["text", "</ol></li>"]
]],
["text", "</ul>"]
]
通过打印的tokens我们可以发现,外层三个token,其中中间的 #类型的token 里面还包含着嵌套,细心的观察,我们知道,当
遇到{{}}的时候会被封装成一个数组,数组里面第一个表示类型(text类型包括标签、文本,name类型表示匹配到{{}},#类型表示要循环),第二个是文本内容,这个数组就是一个 token。全部的 token(数组)合并就形成了一个 tokens。
手写 mustache
手写实现 Scanner 类
前面我们知道,我们使用 mustache 基本用法的时候,是使用 mustache库 提供的 Mustache.render()方法 将模板跟数据进行合并,然后生成 DOM 结构的。底层是如何将我们的模板字符串,编译成上面我们所说的tokens的呢?其实就是使用了一个Scanner扫描器,对模板字符串的 {{ 和 }} 进行截取和过渡。我们来然认识一下 Scanner类 的两个主要方法:
scan函数作用:跳过指定的内容,没有返回值。scanUtil函数作用:让指针扫描模板字符串,直到遇到指定的内容结束,并且能够返回指定内容之前的文本。 那么如何进行字符串的收集?我们需要提供一个标识的变量tail(尾巴随指针改变而改变,包含指针)来进行字符串的收集
了解Scanner工作的两个主要方法之后,我们来理一下其工作流程图:
基本代码实现过程如下:
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾部 一开始就是模板字符串的原长度
this.tail = templateStr
}
// 跳过指定的内容 没有返回值
scan(jump) {
// 字符串的indexOf 返回0 说明第一个就是指定的内容
if (this.tail.indexOf(jump) == 0) {
// 改变指针 直接跳过指定的内容
this.pos += jump.length
// 更新尾部
this.tail = this.templateStr.substring(this.pos)
}
}
// 让指针进行扫描模板字符串,直到遇到指定的内容结束,并且能够返回指定内容之前的文字
scanUtil(stopTag) {
// 记录执行本方法的指针pos的位置
const pos_pack = this.pos
// 当尾巴开头不是指定的内容的时候 说明扫描器还没有找内容
// 当找不到的时候 必须要设置指针长度小于模板字符串的长度,否则会一直找不到会陷入死循环
while (!this.eos() && this.tail.indexOf(stopTag) !== 0 ) {
// 没有找指定的内容则让指针往下移动
this.pos++
// 尾巴跟着指针发生变化
this.tail = this.templateStr.substring(this.pos)
}
// 如果找到 返回前面的文字
return this.templateStr.substring(pos_pack, this.pos)
}
// 指针是否已经到头
eos () {
return this.pos >= this.templateStr.length
}
}
parseTemplelateTokens()方法
这个方法主要的作用就是将HTML变成一个个token。
import Scanner from './Scanner'
import nestTokens from './nestTokens'
export default function parseTemplateTokens(templateStr) {
// 实例化一个扫描器 构造时提供一个参数 专门为模板字符串服务的
// 处理模板字符串 为生成tokens服务
const scanner = new Scanner(templateStr)
// 将token存储到数组中 形成tokens
let tokens = []
// 收集路过的文字内容
let words
while (!scanner.eos()) {
// 收集路过的文字内容
words = scanner.scanUtil('{{')
if (words != '') {
// 标志位 不能去掉类名的空格
let isClass = false
// 去除空格
// 拼接
var _words = ''
for (let i = 0; i < words.length; i++) {
// 判断是否在标签里面
if (words[i] == '<') {
isClass = true
} else if (words[i] == '>') {
isClass = false
}
// 当前项不是空格 拼接上
if (!/\s/.test(words[i])) {
_words += words[i]
} else {
// 是空格 只有在标签内才拼接上
if(isClass) {
_words += ' '
}
}
}
tokens.push(["text",_words])
}
// 跳过指定的内容
scanner.scan('{{')
words = scanner.scanUtil('}}')
if (words != '') {
if (words[0] === '#') {
tokens.push(["#", words.substring(1)])
} else if (words[0] === '/') {
tokens.push(["/", words.substring(1)])
} else {
tokens.push(["name", words])
}
}
scanner.scan('}}')
}
return nestTokens(tokens)
}
好了上面我们已经将模板字符串封装成一个token了,只是将简单的模板字符串封装成token,我们还没有完成循环桥套的功能,下面,我们继续来折叠token,
nestTokens()方法
nestTokens()方法的作用就是 折叠token,将 # 和 / 之间的 tokens能够整合起来,作为它的下标为2的项,从而形成一个 tokens。在写nestTokens时,我们需要知道数据结构中的栈概念,栈先进后出,队列先进先出,在折叠了的时候需要用到数据结构的栈的思路去折叠token。
有了这个栈的思路,我们可以想到,当循环的时候遇到
#符号 会先进 栈(压栈),当遇到 /符号 便会出栈。相关代码如下:
export default function nestTokens(tokens) {
// 组装好的结果数组
const nestTokens = []
// 栈 用来存储 #
let section = []
console.log(tokens);
for (let i = 0; i < tokens.length; i++) {
// 遍历每一项
let token = tokens[i]
// 判断数组第一个项是# 还是 / 还是文本类型
switch (token[0]) {
case "#":
// 将#后面的token存入到当前栈的队尾(栈口)数组的下标为2的项,
// 作为当前数组的子数据
token[2] = []
// 入栈
section.push(token)
// 进栈也要将数组存放到结果数组中
nestTokens.push(token)
break
case "/":
// 出栈
let section_pop = section.pop()
// 将出栈的数组存放到结果数组中
nestTokens.push(section_pop)
break
default:
// 如果栈为空 直接放进结果数组
if (section.length == 0) {
nestTokens.push(token)
} else {
// 不为空 说明栈有数据 取出栈顶并且将#号后面的token数组放到到当前下标为2的栈顶项中
section[section.length - 1][2].push(token)
}
}
}
console.log(nestTokens);
}
捋一下上面代码的执行的大致过程:首先,我们要先声明一个 section数组 作为栈和返回的 结果数组,遍历每一个token,判断当前数组的第一项是否为#或 / ,#入栈,要让当前数组push进栈,/ 则出栈,让当前数组pop出栈,一开始如果一上来栈中没有数据,也就是说当前的数组第一项既不是#也不是/,可以直接存放到结果数组中,当判断有一个数组第一项是 # 的时候,会压入栈中,并且会存放到结果数组中,我们通过上面所说的token可以知道,遇到 # 会在当前的数组下标为2中创建一个数组用来存储数据,上面的 #已经入栈,说明 section栈 已经有了数据,后面的数组数据会全部都存放到栈顶下标为2(也就是当前新进栈的数组)创建好的数组中,直到遇到下一个 # 为止,然后重复再上面的过程。当数组的第一项是/,说明此时要出栈,需要将出栈的数组存放到结果数组中。
以上就是nestTokens的大致流程,再结合下面的图解,相信各位小伙伴们可以更好的理解。
测试结果:
当我们以为真的要写好
nestTokens方法 的时候,打印出来的结果却与我们想象的不符。通过观察我们写的代码可以发现,其实,我们的思路是正确的,但是每一次进栈的时候都会为新的栈顶创建一个数组,然后栈顶并不会成为栈尾的一个子数组。所以,我们需要找一个收集器,能一层一层的往上收集。我们知道了问题出现在哪了,所以对之前的代码进行改造
export default function nestTokens(tokens) {
// 组装好的结果数组
const nestTokens = []
// 栈 用来存储 #
let section = []
// 收集器 一开始指向结果数组 使用引用类型的方式
// 收集器的指向会发生变化 当遇到#的时候 收集器会指向这个token下标为2的新数组
var collector = nestTokens
for (let i = 0; i < tokens.length; i++) {
// 遍历每一项
let token = tokens[i]
// 判断数组第一个项是# 还是 / 还是文本类型
switch (token[0]) {
case "#":
// 收集器中放入这个token
collector.push(token)
// 入栈
section.push(token)
// 改变收集器
// 将#后面的token存入到当前栈的队尾(栈口)数组的下标为2的项,
// 给token添加下标为2的项 并让收集器指向它
collector = token[2] = []
break
case "/":
// 出栈
section.pop()
// 改变收集器为栈结构队尾(栈顶)那项的下标为2的数组
collector = section.length > 0 ? section[section.length - 1 ][2] : nestTokens
break
default:
collector.push(token)
}
}
console.log(nestTokens);
return nestTokens
}
运行结果:
执行之后,刚好是我们想要的子数组嵌套数组的形式,我们就完美的做到了
tokens的折叠。已经完成了从模板字符串到 tokens 的编译,剩下的就是将 tokens与数据进行合并,解析生成DOM。
renderTemplate() 方法
renderTemplate()方法的主要作用就是将:tokens和数据合并,然后生成 DOM字符串。我们先用一个简单的tokens和数据进行合并。
<script src="/xuni/bundle.js"></script>
<script>
const data = {
name: '青峰',
age: 18
}
var templateStr = `<h3>姓名:{{name}},年龄:{{age}}</h3>`
templateEngine.render(templateStr, data)
</script>
/*
将tokens和数据进行合并,生成DOM字符串
*/
export default function renderTemplate (tokens, data) {
console.log(tokens);
console.log(data);
// 结果字符串
let str = ''
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// 判断第一项是text 还是 name 和 #
if(token[0] == "text") {
// 直接将数据拼接到结果字符串中
str += token[1]
} else if (token[0] == "name") {
// 从data中寻找数据
str += data[token[1]]
} else if (token[0] == "#") {
// 循环递归遍历
}
}
console.log(str);
}
我们可以将简单的模板字符串 和 data合并,生成一个DOM字符串,但是需要注意的是,当data数据中嵌套比较深的时候,我们就无法通过 data[token[1]]来获取data中的数据了。比如:data['a.b.c'],此时会将[]中的'a.b.c'变成字符串,然后在当前对象的直接寻找改字符串对应的数据,显然这样会返回一个undefined 是找不到的。所以我们需要对data['a.b.c']的形式进行处理。
lookup()方法
当模板字符串中存在比如 item.name 的形式的时候,我们需要对此字符串进行处理,然后获取对应的数据。
<script>
const data = {
person: {
name: '青峰',
age: 18
}
}
var templateStr = `<h3>姓名:{{person.name}},年龄:{{person.age}}</h3>`
templateEngine.render(templateStr, data)
</script>
/**
* 处理嵌套对象的获取最底层对象的数据
* @param {*} obj 传入的对象 原始对象
* @param {*} keyName 传入的字符串比如 person.name
* @returns 返回获取嵌套对象里的数据
*/
export default function lookup(obj, keyName) {
// 对data数据中普通数组的处理
if (keyName !='.'){
// 使用数组的redecu方法进行遍历
return keyName.split('.').reduce((pre, next) => {
return pre[next]
}, obj)
}
return data[keyName]
}
/*
将tokens和数据进行合并,生成DOM字符串
*/
import lookup from "./lookup";
export default function renderTemplate (tokens, data) {
console.log(tokens);
console.log(data);
// 结果字符串
let str = ''
for(let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// 判断第一项是text 还是 name 和 #
if(token[0] == "text") {
// 直接将数据拼接到结果字符串中
str += token[1]
} else if (token[0] == "name") {
// 从data中寻找数据 拼接到结果字符串中
str += lookup(data, token[1])
} else if (token[0] == "#") {
// 循环递归遍历
}
}
console.log(str);
}
处理好data中的对象嵌套的问题之后,最后就只剩下判断#的递归情况了。
parseArray()方法
parseArray()方法 主要用来递归遍历 #里面的数组。需要注意的是:当data中是一个简单数组的时候,我们是直接以 {{.}} 的形式进行渲染的,这此之前,我们是没有对‘.’属性进行处理的,所以,我们打递归的时候需要补充一个‘.’属性,并值为本身,才会正确的渲染出数据。
import lookup from "./lookup";
import renderTemplate from "./renderTemplate";
/**
* 处理数组结构renderTemplate实现递归
* @param {*} token 传入的包含#的一项数据 ['#','hobbies',[]]
* @param {*} data 数据
* 调用的次数用data的长度决定
*/
export default function parseArray (token, data) {
// 获取这个数组中需要的数据
var v = lookup(data, token[1])
// 结果字符串
var str = ''
// 遍历的是数据 而不是 tokens data数组中有几条数据,就需要遍历多少次
for(let i = 0; i < v.length; i++) {
str += renderTemplate(token[2],{
// 当数据是一个简单数组并不是对象数组的时候
// 我们需要补一个‘.’属性,为当前v[i]数据本身
...v[i],
'.': v[i]
})
}
return str
}
templateEngine()方法
import parseTemplateTokens from './parseTemplateTokens'
import renderTemplate from './renderTemplate'
window.templateEngine = {
render(templateStr, data) {
// 将模板字符串组成tokens
const tokens = parseTemplateTokens(templateStr)
// 将组成的tokens与数据进行结合,生成DOM字符串
let domStr = renderTemplate(tokens, data)
// 返回DOM字符串
return domStr
}
}
<!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>
<div class="container"></div>
<script src="/xuni/bundle.js"></script>
<script>
const templateStr = `
<ul>
{{#array}}
<li class='qf'>
{{name}}的爱好
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/array}}
</ul>
`
const data = {
array: [
{ name: 'Alex', hobbies:['打篮球', '羽毛球']},
{ name: 'Jack', hobbies:['游泳', '唱歌']},
{ name: '青峰', hobbies:['玩游戏', '踢足球']},
]
}
// var templateStr = `<h3>姓名:{{person.name}},年龄:{{person.age}}</h3>`
const domStr = templateEngine.render(templateStr, data)
const container = document.querySelector('.container')
container.innerHTML = domStr
</script>
</body>
</html>
运行代码输出结果:
以上就是Vue源码解析 中 mustache源码 中的主干部分,本文对一些细枝末节并没有进行处理,但是总体的功能基本都能够实现。 读完你可能会发现,本文章中最精彩的部分就是 nestTokens方法 中使用 收集器 和 栈 的思想,这种思想是很值得我们去学习的!!这也是读源码的魅力,你会发现里面的算法真的很妙!!!
文章中相关源码已发上gitee,需要获取的小伙伴可以获取源码戳我!!!
最后,读完篇文章希望对你们理解 Vue底层 是如何进行 v-for渲染 有所帮助,小编希望各位大佬们能够点一下赞!!