Vue源码探析之mustache模板引擎学习笔记+手写完成mustache功能(阉割版)

一、模板引擎是什么?

模板引擎是将数据变为视图最优雅的解决方案。

1.数据转为视图的发展史

1.1纯DOM法:

效果图:
在这里插入图片描述

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')

        for (var i =0 ; i<arr.length;i++){
      
      
            // 没遍历一项,都要用DOM方法去创建li标签
            let oLi = document.createElement('li')
            // 创建hd这个div
            let hdDiv = document.createElement('div')
            hdDiv.className = 'hd'
            hdDiv.innerText = arr[i].name + '的基本信息'
            // 创建bd这个div
            let bdDiv = document.createElement('div')
            bdDiv.className = 'bd'
            // 创建三个p标签
            let p1 = document.createElement('p')
            p1.innerText = '姓名:' + arr[i].name

            let p2 = document.createElement('p')
            p2.innerText = '年龄:' + arr[i].age

            let p3 = document.createElement('p')
            p3.innerText = '性别:' + arr[i].sex

            //创建的节点都是孤儿节点,所以必须上树才能被用户看到
            oLi.appendChild(hdDiv)

            hdDiv.appendChild(bdDiv)

            bdDiv.appendChild(p1)

            bdDiv.appendChild(p2)

            bdDiv.appendChild(p3)
            
            list.appendChild(oLi)
        }
    </script>
</body>
</html>

总结:
纯DOM操作数据转换成视图每一步都在操作DOM,这样不仅页面开销大,而且非常麻烦,每次都需要创建并上树,数据和视图耦合度很高,也很难辨识,代码阅读性也差。

1.2数组join法:

前期背景:
由于字符串" " ’ ‘定义的字符内容是不允许换行, 反引号是ES6的新语法,以前也没有,但是数组可以换行展示,能够显示结构的层次,然后通过join(’')方法可以实现字符拼接显示

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')
        // 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
        for (let i =0; i< arr.length;i++){
      
      
            list.innerHTML += [
                '<li>',
                '       <div class="hd">' + arr[i].name + '的信息</div>',
                '       <div class="bd">',
                '           <p>姓名:'+ arr[i].name +'</p>',
                '           <p>年龄:'+ arr[i].age +'</p>',
                '           <p>性别:'+ arr[i].sex +'</p>',
                '       </div>',
                '</li>'
            ].join('')
                    
        }
    </script>
</body>
</html>

总结:
巧妙的运用了数组join方法,使得代码量大大减少了,同时由于可以换行,结构也没有那么晦涩了,但是代码和数据依旧有些粘连,无法将视图和数据分离开来

1.3ES6的反引导法:

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')
        // 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
        for (let i =0; i< arr.length;i++){
      
      
            list.innerHTML += `
                <li>
                    <div class='hd'>${ 
        arr[i].name}的基本信息</div>
                    <div class='bd'>
                        <p>姓名:${ 
        arr[i].name}</p>
                        <p>年龄:${ 
        arr[i].age}</p>
                        <p>性别:${ 
        arr[i].sex}</p>
                    </div>
                </li>
            `
            

        }
    </script>
</body>
</html>

总结:
相较于数组的话,简化了非常多的写法,减轻书写的难度,毕竟是新出的语法,效果和之前两种没有太大差异,但是听讲师说,这样写多了一个字符串解析得过程,相较于最笨的写法是慢点得,但是书写方便,我觉得还可以通过createDocumentFragment()方法降低开销

1.4模板引擎:

<script>
        var templateStr = `
            <ul>
                {
       
       {#arr}}
                    <li>
                        <div class='hd'>{
       
       {name}}的基本信息</div>
                        <div class='bd'>
                            <p>姓名:{
       
       {name}}</p>
                            <p>年龄:{
       
       {age}}</p>
                            <p>性别:{
       
       {sex}}</p>
                        </div>
                    </li>
                {
       
       {/arr}}
            </ul>
        `
        var data = {
      
      
                 arr : [
                    {
      
      "name":"某可","age":18,"sex":"男"},
                    {
      
      "name":"某鸿","age":18,"sex":"男"},
                    {
      
      "name":"小李","age":5,"sex":"女"}
                ]
        }

        var domStr = mustache.render(templateStr,data)
        var container = document.getElementById('container')
        container.innerHTML = domStr
    </script>

2.mustache模板引擎实现原理:

①将模板字符串编译成tokens形式

②将tokens结合数据,解析为dom字符串

在这里插入图片描述

1.tokens:

tokens是一个JS得嵌套数组,就是模板字符串得JS表示
它是"抽象语法树"、"虚拟节点"等等的开山鼻祖
模板字符串:

<h1> 我买了一个{
   
   {thing}},好{
   
   {mood}}啊</h1>
tokens:
其中每一项都是一个token,例如:["text",",好"],加起来就是tokens
[
	["text","<h1> 我买了一个"],
	["name","thing"],
	["text",",好"],
	["name","mood"],
	["text","啊</h1>"]
]

存在循环的时候会编译成嵌套更深的tokens

<div>
	<ul>
		{
   
   {#arr}}
		<li>{
   
   {.}}</li>
		{
   
   {/arr}}
	</ul>
</div>


[
	["text","<div><ul>"],
	["#","arr",[
		["text","<li>"],
		["name","."],
		["text","</li>"]
	]],
	["text","</ul></div>]
]

二、手写实现mustache库

1.创建Scanner类

1.1作用:

扫描字符串,为模板字符串转换成tokens做服务

1.2代码:

定义Scanner类
作用是查找{ {或}}将内容进行分割
export default class Scanner {
    
    
    constructor(templateStr){
    
    
        // 将模板字符串写到自己的属性上,或者说实例接收参数
        this.templateStr = templateStr
        // 指针
        this.pos = 0
        // 尾巴,开始就是原文
        this.tail = templateStr
    }
    // 功能简单,就是走过指定内容,没返回值
    scan(tag){
    
    
        if(this.tail.indexOf(tag) == 0){
    
    
            // tag有多长,就后移多少位
            this.pos += tag.length
            // 改变尾巴
            this.tail = this.templateStr.substring(this.pos)
        }
    }

    // 让指针进行扫描,直到遇到{
    
    {}}指定内容,并且返回结束之前的路过的文字内容
    scanUtil(stopTag){
    
    
        // 记录一下执行本方法的时候pos的值
        const pos_backup = this.pos
        // 当尾巴的开头不是stopTag的时候,说明还没扫到
        //&&后内容防止死循环
        while(!this.eos() && this.tail.indexOf(stopTag) != 0 ){
    
    
            this.pos++
            // 改变尾巴为当前指针这个字符开始,到最后的全部字符
            this.tail = this.templateStr.substring(this.pos)
        }
        return this.templateStr.substring(pos_backup,this.pos)
    }

    // 指针是否已经到头,返回布尔值
    eos(){
    
    
        return this.pos >= this.templateStr.length
    }
}
创建并暴露parseTemplateToTokens方法:
作用是将模板字符转换成tokens数组
import Scanner from './Scanner.js'

// 将模板字符串变为tokens数组

import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js';

// 将模板字符串变为tokens数组

export default function parseTemplateToTokens(templateStr){
    
    
    var tokens = []
    // 创建扫描器
    var scanner = new Scanner(templateStr)
        // 让扫描器工作
        var words;
        while(!scanner.eos()){
    
    
            // 收集标记出现之前的文字
            words = scanner.scanUtil('{
    
    {')
            if(words !=''){
    
    
                // 需要去掉空格,但是不能去掉class
                var isInJJH = false
                var _words =''
                for (let i =0;i<words.length;i++){
    
    
                    // 判断是否在标签里面
                    if(words[i] =='<'){
    
    
                        isInJJH = true
                    }else if (words[i] =='>'){
    
    
                        isInJJH = false
                    }
                    // 如果这项不是空格,拼接
                    if(words[i]!=' '){
    
    
                        _words += words[i]
                    }else{
    
    
                        if(isInJJH){
    
    
                            _words+=words[i]
                        }
                    }
                }
                tokens.push(['text',_words])
            }
            scanner.scan('{
    
    {')

            // 收集{
    
    {}}之间的文字
            words = scanner.scanUtil('}}')
            if(words !=''){
    
    
                // 判断首字母是不是#
                if (words[0] == '#'){
    
    
                    //存起来,下标从1开始,表示是#
                    tokens.push(['#',words.substring(1)])
                }else if (words[0] == '/'){
    
    
                    //存起来,下标从1开始,表示是/
                    tokens.push(['/',words.substring(1)])
                }else{
    
    
                    tokens.push(['name',words])
                }
                
            }
            scanner.scan('}}')
        }
        // 返回折叠收集的tokens
        // console.log(tokens)
    return nestTokens(tokens)
}
使用测试:
import parseTemplateToTokens from './parseTemplateToTokens'

window.YSZ_TemplateEngine = {
    
    
    render(templateStr,data){
    
    
        console.log("render函数被调用了")
        // 将模板字符串变成tokens数组
        var tokens = parseTemplateToTokens(templateStr)
    }
}
输出结果:

在这里插入图片描述

											图2.1

2.创建nestTokens方法

作用是将生成的Tokens之间的嵌套关系表达出来,利用了栈的先进后出原理
代码部分:
// 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项

export default function nestTokens(tokens){
    
    
    // 结果数组
    var nestedTokens = []
    //  收集器,初始指向nestedTokens
    // 充分利用了引用类型的特点,这里的指向会改变,当遇到#的时候,会指向数组下标为2的位置,没有就创建
    var collector = nestedTokens
    // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前
    // 操作的这个tokens小数组
    var sections = []

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

        switch(token[0]){
    
    
            case '#':
                // 收集器中放入这个token
                collector.push(token)
                // 压栈(入栈)
                sections.push(token)
                // 收集器要换人了,给token添加下标为2的项,并且让收集器指向它
                collector = token[2] = []
                break
            case '/':
                // 出栈 pop()会返回刚刚弹出的项
                let section_pop = sections.pop()
                //改变收集器为栈结构队尾(队尾就是栈顶)那项的下标为2的数组
                collector = sections.length >0 ? sections[sections.length - 1][2] : nestedTokens
                break
            default:
                collector.push(token)
        }
    }


    console.log(nestedTokens)
    return nestedTokens
}
测试结果:

在这里插入图片描述

实现思路及代码分析:
  • 首先,由于tokens的嵌套关系的不确定性,不知道内部还嵌套几层,比如students嵌套item.hobbies,hobbies内部可能依旧嵌套很多层,所以从嵌套关系来说,具有不确定性,但是有一个数据结构恰好能解决这类问题,那就是栈,栈具有先进后出,后进先出的特性,遇到#就入栈,遇到/就出栈,这样studens之所以不好处理其实就是内部层数的不确定性,但是根据栈的特性,students是最先入栈的,也就是处于栈底,所以在入栈完成之后,进行出栈处理的时候,肯定是最后处理他的,那反之则是最内层的一定是没有嵌套的也就是最简单的结构,同时也是最先出栈或被处理的数据,那么一旦这一层解决了,它的上一层也就好处理了,依次类推,当students出栈的时候,他的内部数据已经被处理完毕了,也就是确定了。
    算法逻辑确定后,就开始代码实现,首先肯定是需要一个for循环,查找tokens内的所有数据,并且需要一个定义sections数组,通过push从队尾入栈,他的作用是存储入栈数组,如这里会先入栈students然后hobbies,那么对于sections数组来说现在就是一个栈结构了,数组的起始项是栈底,数组的尾部是栈顶;如此便有栈结构,有了栈结构,还需要完善出入栈时需要进行的操作,这里为了完成嵌套数据的收集,定义了一个新的变量collector(这步非常精妙,因为如果只有nestedTokens进行收集数据遇到#和/的时候需要做过多的判断操作也就是if语句,引入collector变量使得逻辑更清晰),它的作用就是收集变量,但是与nestedTokens不同的是,它起始通过引用类型特性指向nestedTokens变量,如果未遇到#,他与nestedTokens没有任何区别(因为他的引用类型一直是nestedTokens,所以它push就等于nestedTokens的push操作),但是当遇到#,他的指向就会发生变化,他会指向最新的数组,或者说更小的数组,collector = token[2] = []由结果处可看出在这里插入图片描述,token[2]
    = [] 就是students内部嵌套的数组,先创建再通过collector=token[2]使其指向最新数组,这时候收集操作就是为students的这个token数组的下标为2的项做收集操作,此时若是依旧是普通数据,collector一直为students的token[2]做push操作直到遇到新的#,也就是hobbies,那么这个时候又重复步骤,先将hobbies入栈,然后调整collector指向最新的数组也就是hobbies这个token[2]项,然后为其收集数据,如果hobbies是最底层嵌套,那么根据栈的特性,我们知道接下来肯定是hobbies先做出栈处理,通过之前scanner中生成的tokens图2.1也可看出确实是先执行hobbies这项的出栈,出栈的表示是/,当遇到一个/就代表一个嵌套结构的结束,当遇到hobbies的/结构时候,证明hobbies的数据收集已经完成,并且收集数据都在hobbies的token[2]这个数组项中存储如图,

在这里插入图片描述
这样最内层的hobbies数据收集完成,那么这一项就需要出栈,并且将collector指回students这个token[2]项继续为其收集(这里的逻辑是这样的,开始为nestedTokens收集数据,但是遇到了#,那么collector就去为students办事了,但是途中又遇到了#,它又去为hobbies办事了,如今hobbies是最底层了,遇到hobbies的/这个标识的时候就做hobbies的出栈处理,那么它又回来为students继续做事了,收集完剩余最后一项这一项,就会遇到students的标识/,这个时候进行students这项做出栈处理,它又回去帮nestedTokens做事了,收集完最后一项,此时全部收集完成结构显示也如上图,是预期结构),这里需要注意的是当students这层收集完成后,其实栈sections中已经为空了,那么需要collector = sections.length >0 ? sections[sections.length - 1][2] : nestedTokens出栈的时候做特殊处理,当栈内为空的时候说明已经到了最外层也就是nestedTokens层,这时候让collector指回nestedTokens即可,这里巧妙的利用了collector及其引用关系来做数据的收集操作,这样做的好处就是当遇到#的时候只需要改变collector收集器的指向即可,无需为nestedTokens做过多的判断,他就做一个干干净净收集装置,遇到#入栈以及/出栈的问题交由收集器去处理即可,如此便完成tokens嵌套结构的处理。

3.将tokens结合数据拼接成DOM字符串形式:

1.创建renderTemplate函数

作用:
通过接收tokens和data数据,将两者结合返回字符串类型的DOM结构

import lookup from "./lookup"
import parseArray from "./parseArray";
export default function renderTemplate(tokens,data){
    
    
    console.log(data)
    // 结果字符串
    var resultStr = '';
    // 遍历tokens
    for(let i=0;i<tokens.length;i++){
    
    
        // 拿到每组token
        let token = tokens[i]
        // 类型判断
        if(token[0]=='text'){
    
    
            // 字符类型直接拼接
            resultStr +=token[1]
        }else if(token[0] =='name'){
    
    
            // resultStr +=data[token[1]]
            resultStr += lookup(data,token[1])
        }else if(token[0]=='#'){
    
    
            resultStr += parseArray(token,data)
        }
    }
    console.log(resultStr)
    return resultStr
}

问题:由于 resultStr +=data[token[1]]无法识别.语法,当数据结构为多层嵌套的时候如下图2,token[1]就是a.m.n,显然data[‘a.m.n’]是无法错误的书写方式,结果如图1,确实是undefined,为了解决这个问题需要创建lookup函数在这里插入图片描述 图1
在这里插入图片描述

													图2

2.创建lookup函数

背景:
由于无法是被.语法,lookup函数就当遇到一个多层结构,返回最底层数据,如果是普通数据直接返回结果即可

代码
export default function lookup(dataObj,keyName){
    
    
    // 看看keyName里面有没有.符号,但是本身不能是.,或者说不能只是.
    if(keyName.indexOf('.') != -1 && keyName != '.'){
    
    
        // 如果有就拆开
        var keys = keyName.split('.')
        // 周转变量方便逐层查找
        var temp = dataObj
        for(let i=0;i<keys.length;i++){
    
    
            temp = temp[keys[i]]

        }
        return temp
    }
    // 如果没点符号
    return dataObj[keyName]
}

结果:
只需要在renderTemplate函数导入并修改resultStr += lookup(data,token[1])即可,结果如下图,已经可以识别多层结构了。

在这里插入图片描述

3.创建parseArray函数

作用是处理嵌套数组问题,

代码:
import lookup from "./lookup"
import renderTemplate from "./renderTemplate"
// 处理数组,结合renderTemplate实现递归
// 这里的token并不是tokens,而是token=> ['#','students',[]]
// 简而言之就是这个token里面包含数组

// 重点:
// 这里需要递归调用renderTemplate函数,至于调用多少次和data数据有关
// 假设data的形式是:
// {
    
    
//     students : [
//         {"name":"某可","age":18,"sex":"男"},
//         {"name":"某鸿","age":18,"sex":"男"},
//         {"name":"小李","age":5,"sex":"女"}
//     ]
// }
// 那么就需要循环调用三次,因为数组的长度是三

export default function parseArray(token,data){
    
    
    console.log(token,data)
    // 拿到对于数据的名字 这里是students
    var v = lookup(data,token[1])
    var resultStr = ''
    // 遍历v数组,v一定是数组(原本可以跟Boolean值这里是简化版)
    for(let i=0;i<v.length;i++){
    
    
        // 这里和h函数的渲染有异曲同工之妙,token[2]里面存的又是类似于最外层这种tokens结构,v[i]就是data数据项
        // ★这里有个难点,如果是复杂属性例如上述students中第一项,外层是对象,内部是键值对属性表示
        // 那么v[i]传递数据肯定可以被拿到,因为这样模板就能通过name直接拿到里面的值如<p>姓名:{
    
    {name}}</p>,
        // 但是如果是数组["香蕉","苹果","梨子"],这样v[i]传递的数据就无法被拿到,因为无法识别模板中的.例如<li>{
    
    {.}}</li>
        // 其实就是因为数据是数组,而模板取数据通过属性名,数组没有属性名访问,一般是下标访问,所以用.符号代替
        // 所以这里需要取识别.符号,也就是简单数组数据类型
        // resultStr += renderTemplate(token[2],v[i])
        resultStr += renderTemplate(token[2],{
    
    
            // 如果是对象类型,证明已经有属性名了,那么直接拆分属性放到{}对象里面就好了
            ...v[i],
            // 如果是简单数组类型,那么就将.符号作为它的属性名,v[i]作为他的属性值,这样就能通过.拿到他的属性值了
            '.':v[i]
        })
        // 上面使用外层{}接受,将简单数组数据包装成{}类型,.符号就是他的属性名,属性值就是v[i]
    }
    return resultStr
}
数据和模板:

在这里插入图片描述

运行结果:

在这里插入图片描述
代码难点:
首先,parseArray函数是在renderTemplate中调用,其实parseArray可以不用分割出来,只看代码量来说并不多,但是其中逻辑比较难理解,从我的代码注释量就可以看出来了,所以单独抽出,这里首先用到的一个思想就是递归,如果有看过h函数渲染虚拟节点的朋友肯定对这里非常熟悉,h函数在遇到children属性的时候也是通过递归调用h函数本身来实现渲染子节点的,这里实现其实很简单但是想到需要些难度,renderTemplate的作用就是将tokens转换成DOM字符串拼接,遇到text和name属性都比较好解决,text就是字符串形式的直接拼接即可,而name属性是需要取data数据中取数据的,通过lookup函数我们也能轻松取到值,但是当遇到#的时候,代表其内部还有嵌套结构,换而言之就是这个token也需要被作为一个tokens对待,和h函数非常相似,所以这里递归递归调用了renderTemplate函数,又将除#之外的全部拼接完成后返回,其实归根结底最终一定是没有#的语句,这里循环调用的次数取决于data中的数据类型,如果如样例students,那么只需要循环调用三次就能完成全部模板的创建了,这里其实和我们Vue中常见的v-for达到的效果是一样的,最后只需要将生成的DOM字符串赋值给需要的innerHTML即可在页面中显示。

参考视频:
【尚硅谷】Vue源码解析之mustache模板引擎
图片部分来源:
【尚硅谷】Vue源码解析之mustache模板引擎

猜你喜欢

转载自blog.csdn.net/weixin_54515240/article/details/130198895