Vue源码解析之 mustache 模板引擎

1,375 阅读10分钟

mustache 模板引擎.png 本文章主要讲解的是 vue 中 mustache 模板引擎的底层机理是如何实现的,主要分为四大部分:先了解什么是模板引擎,掌握 mustache 基本使用,分析 mustache 的底层核心机理,最后手写实现 mustache库 底层代码,拒绝纸上谈兵。整个过程是一个循序渐进的过程,本文章全是满满的干货,确保你能真正的理解 Vue模板引擎 底层是如何工作的。最后希望各位大佬点个!!好了话不多说,开始 mustache模板引擎 之旅!!

知识储备

  • 会写一些J avaScript 常见算法,比如递归二维数组遍历等;
  • 熟悉 ES6 的常用特性,比如 let箭头函数解构赋值等等;
  • 了解 webpackwebpack-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)
}

image.png 看到上面创建纯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('')    
}

image.png 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>

image.png

循环嵌套数组

 <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>

image.png

循环简单数组

<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>

image.png

不循环

<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>

image.png

布尔值

在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()方法 用来匹配正则表达式:

  • 第一个参数:要匹配的正则
  • 第二个参数:可以是替换的字符串,也可以是一个函数
  • 第二个参数是函数的形式:第一个参数表示匹配到的正则部分,第二个参数表示匹配到的字符,第三个表示匹配到的字符所在位置,第四个表示需要匹配的字符串。一般来说第二个参数是我们想要的数据。 image.png
<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>

image.png 一个最简单的模板引擎的实现机理就写好了,利用的是正则表达式中的replace()方法replace()方法的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,再结合data对象,即可进行智能的替换。

底层 tokens 思想

通过上面,我们知道mustache不是用正则表达式来实现的,那么它底层是如何实现的呢?其实它实现的机理就是下面的那张图。 image.png 我们解读一下上面图片的基本流程:
要将模板字符串先编译成 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>"]
]

image.png 通过打印的tokens我们可以发现,外层三个token,其中中间的 #类型的token 里面还包含着嵌套,细心的观察,我们知道,当遇到{{}}的时候会被封装成一个数组,数组里面第一个表示类型(text类型包括标签、文本,name类型表示匹配到{{}},#类型表示要循环),第二个是文本内容,这个数组就是一个 token。全部的 token(数组)合并就形成了一个 tokens

手写 mustache

手写实现 Scanner 类

前面我们知道,我们使用 mustache 基本用法的时候,是使用 mustache库 提供的 Mustache.render()方法 将模板跟数据进行合并,然后生成 DOM 结构的。底层是如何将我们的模板字符串,编译成上面我们所说的tokens的呢?其实就是使用了一个Scanner扫描器,对模板字符串的 {{}} 进行截取和过渡。我们来然认识一下 Scanner类 的两个主要方法:

  • scan函数 作用:跳过指定的内容,没有返回值。
  • scanUtil函数 作用:让指针扫描模板字符串,直到遇到指定的内容结束,并且能够返回指定内容之前的文本。 那么如何进行字符串的收集?我们需要提供一个标识的变量tail(尾巴随指针改变而改变,包含指针)来进行字符串的收集

image.png 了解Scanner工作的两个主要方法之后,我们来理一下其工作流程图:

image.png

基本代码实现过程如下:

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。

image.png 有了这个栈的思路,我们可以想到,当循环的时候遇到 #符号 会先进 栈(压栈),当遇到 /符号 便会出栈。相关代码如下:

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的大致流程,再结合下面的图解,相信各位小伙伴们可以更好的理解。 image.png

测试结果: image.png 当我们以为真的要写好 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
}

运行结果:

image.png 执行之后,刚好是我们想要的子数组嵌套数组的形式,我们就完美的做到了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);
}

image.png
我们可以将简单的模板字符串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);
}

image.png

处理好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>

运行代码输出结果:

image.png

以上就是Vue源码解析mustache源码 中的主干部分,本文对一些细枝末节并没有进行处理,但是总体的功能基本都能够实现。 读完你可能会发现,本文章中最精彩的部分就是 nestTokens方法 中使用 收集器 的思想,这种思想是很值得我们去学习的!!这也是读源码的魅力,你会发现里面的算法真的很妙!!!
文章中相关源码已发上gitee,需要获取的小伙伴可以获取源码戳我!!!

最后,读完篇文章希望对你们理解 Vue底层 是如何进行 v-for渲染 有所帮助,小编希望各位大佬们能够点一下!!