Vue源码学习之mustache模板引擎

Vue源码学习之mustache模板引擎

该博文是在学习尚硅谷的vue源码教程同时做的笔记。课程都可以在b站搜到的哦。

mustache模板引擎

什么是模板引擎

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

历史上出现的数据变为视图的方法:

  • 纯DOM法:非常笨拙,没有实战的价值
  • 数组join法:曾几何时非常流行
  • ES6的反引号法:${a}
  • 模板引擎:解决数据变为视图的最优雅的方法

纯DOM法:

const arr = [
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
]
let list = docunment.getElementById('list')
for(let i = 0;i<arr.length; i++){
    let oLi = document.createElement('li')
    oLi.innerText = arr[i].name
    
    list.appendChild(oLi)
}

数组join法:

const arr = [
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
]
let list = docunment.getElementById('list')
for(let i = 0;i<arr.length; i++){
    list.innerHTML += [
        '<li>'
        '	<p>'+arr[i].name+'/p>'
        '</li>'
    ].join('')
}

ES6的反引号法:

const arr = [
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
    {"name":xxx,"age":12},
]
let list = docunment.getElementById('list')
for(let i = 0;i<arr.length; i++){
    list.innerHTML += `
    	<li>
    		<p>${arr[i].name}</p>
    	</li>
    `
}

mustache的基本使用

  • 官方github:https://github.com/janl/mustache.js
  • mutache是最早的模板引擎库,比vue诞生的早的多,他的底层实现机理在当时是非常有创造性的、轰动性的,为后续模板引擎的发展提供了崭新的思路

必须引入mustance库,可以在bootcdn.com找到他

循环对象数组

Mustache.render(templateStr,data)负责循环对象数组并填充好

let str = `
    {{#arr}}
        <li>{{name}}/li>
    {{/arr}}
    <p>{{time}}</p>
`
const data= {
    arr = [
        {"name":xxx,"age":12},
        {"name":xxx,"age":12},
        {"name":xxx,"age":12},
    ],
    time:'2022'
}
Mustache.render(str,data)

也可以循环简单数组

let str = `
    {{#arr}}
        <li>{{.}}/li>
    {{/arr}}
`
const data = {
    arr=['x','y','z']
}
Mustache.render(str,data)

也可以数组嵌套循环

const data = {
    arr:[
        {name:'xxx',hobbies:['x','y','z']},
        {name:'xxx',hobbies:['x','y','z']},
        {name:'xxx',hobbies:['x','y','z']}
    ]
}
let str = `
    {{#arr}}
        <li>{{name}}/li>
        {{#hobbies}}
            {{.}}
        {{/hobbies}}
    {{/arr}}
`
Mustache.render(str,data)

data也可以传入布尔值,效果类似v-if

const data = {
    boolean:false
}
let str = `
    <div>
        {{#bollean}}
            <p>xxx</p>
        {{/bollean}}
    </div>
`
Mustache.render(str,data)

解决反引号繁杂的问题

<script type="text/template" id="first">
    <div>
        {{#bollean}}
            <p>xxx</p>
        {{/bollean}}
    </div>
</script>
<script>
    let str = document.getElementById('first')
    const data = {
        boolean:false
    }
    Mustache.render(str,data)
</script>

mustache的底层核心机理

mustache不能用简单的正则表达式思路实现

较为简单的时候可以用正则实现

let templateStr = `<h1>我买了一个{{thing}},好{{mood}}</h1>`
var data = {
    thing:'华为',
    mood:'开心'
}
function render(templateStr,data){
    //利用正则找到{{}}内的值 和 replace函数进行替换
    return templateStr.replace(/\{\{\(\w+)\}/g,function(findStr,$1){
        return data[$1]
    })
}

var result = render(templateStr,data)

当较为复杂时不能使用,较为复杂时采用以下机制
在这里插入图片描述
)]

  • tokens是一个js的嵌套数组,说白了就是模板字符串的js表示
  • 它是“抽象语法树”、“虚拟节点”等等的开山鼻祖
//模板字符串
let str = `<h1>我买了一个{{thing}},好{{mood}}</h1>`
//经过编译
//tokens
let token = [
    ["text","<h1>我买了一个"],
    ["name","thing"],
    ["text","好"]["name","mood"]
    ["text","啊</h1>"]
]

当模板字符串中有循环存在时,他会被编译为嵌套更深的tokens

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

mustache库底层重点要做两个事情

  • 将模板字符串编译为tokens形式
  • 将tokens结合数据,解析为dom字符串

手写实现mustache库

使用webpack和webpack-dev-serve构建

  • 手写Scanner类
  • 手写html=>tokens
  • 手写将tokens=>注入数据
<script>
    let tempalteStr = `<h1>我买了一个{{thing}},好{{mood}}</h1>`
    var data = {
        thing:'华为',
        mood:'开心'
    }
    TemplateEngine.render()
</script>
import parseTemplateToTokens from './parseTemplateToTokens.js'
import renderTemplate from './renderTemplate.js'
window.TemplateEngine = {
    render(templateStr,data){
        //调用parseTemplateToTokens函数,让模板字符串能够变为tokens数组
        let tokens = parseTemplateToTokens(templateStr)
        //调用renderTemplate函数,让token变为DOM字符串
        let domStr = renderTemplate(tokens,data)
        return domStr
    }
}

新建一个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有多长,比如{{是2,就让他指针后移多少位
            this.pos += tag.length
            //也要改变尾巴,位当前移动后指针的后面的字符串
            this.templateStr.substring(this.pos)
        }
    }
    //让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUtil(stopTag){
        //记录一下执行本方法时候pos的值
        const pos_backup = this.pos
        //当尾巴不是stopTag的时候,就说明还没有扫描到stopTag
        //&& 为了防止因为找不到且到头了而导致死循环
        while(this.tail.indexOf(stopTag)!=0 && this.post<this.templateStr.length>){
            this.pos++
            //改变尾巴从当前指针到这个字符开始的字符
            this.tail = this.templateStr.substring(this.pos)
        }
        return this.tempalteStr.substring(pos_backup,this.pos)
    }
}

新建一个html和token转化的js

import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js'
export default function parseTemplateToTokens(templateStr){
    var tokens = []
    let words;
    //扫描器
    let scanner = new Scanner(templateStr)
    //循环到指针遍历到尾巴
    while(scanner.pos != templateStr.length){
        //寻找{{并收集指针走过的text
        words = scanner.scanUtil('{{')
        if(words != ''){
            //去掉空格
            let isInJJH = false//不在标签内
            //空白字符串
            let _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] == '#'){
                tokens.push(['#',words.substring(1)])
            }else if(words[0] == '/'){
                tokens.push(['/',words.substring(1)])
            }else {
                tokens.push(["name",words])
            }
        }
        //跳过}}
        scanner.scan('}}')
    }
    //返回折叠收集的tokens
    return nestTokens(tokens)
}

新建一个nestTokens.js用于折叠tokens,将#和/之间的tokens能够整合起来,解决嵌套问题
十分精妙及重要

sections栈是展示层级的关系,collector是根据层级移动的指针窗口,nestedTokens是具体的层级内容展示

export default function nestTokens(tokens){
    let nestedTokens = []
    //栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组当前操作的这个tokens小数组
    let  sections = []
    //定义一个收集器,天生指向nestedTokens
    //收集器的指向会不断的变化
    let collector = nestedTokens
    //遇见#入栈,遇见/出栈
    for(let i = 0;i<tokens.length;i++){
        //遍历传入的tokens并获取每一个token
        let token = tokens[i]
        //对每一个token的类型进行判断
        switch(token[0]){
            case '#':
                //收集器放入这个token
                collector.push(token)
                //入栈
                sections.push(token)
                //收集器要换人了
                //给这个token下标为2的项创建一个数组用于收集子元素,并由收集器指向
                collector = token[2] = []
                break;
            case '/':
                //出栈,回到上一级
                sections.pop()
                //改变收集器为栈队尾
                collector = secitons.length > 0?sections[sections.length-1][2]:nestedTokens
                break;
            default:
                collector.push(token)
        }
        
    }

    
    return nestedTokens
}

新建一个renderTemplate.js,让tokens变为DOM字符串

import lookup from './lookup.js'
import parseArray from './parseArray.js'
export default function renderTemplate(tokens,data){
    //结果字符串
    let resStr = ''
    //遍历tokens
    for(let i = 0; i < tokens.length; i++){
        let token = tokens[i]
        //判断类型
        if(token[0] == 'text'){
            //text类型直接拼起来
            resStr += token[1]
        }else if(token[0] == 'name'){
            //name要调用lookup函数找到对象中的数据
            //lookup可以防止a.b.c的情况
            resStr += lookup(data,token[1])
        }else if(token[0] == '#'){
            resStr +=  parseArray(token,data)
        }
    }
}

当toke为name时我们要在data中寻找这个数据,当data数据结构比较复杂时,无法用object.name的方法访问数据

新建一个lookup.js解决上面的问题

//可以在dataObj对象中,寻找用连续点符号的keyName属性
export default function lookup(dataObj,keyName){
    //看看keyName中有没有点符号,但不能是.本身
    if(keyName.indexOf('.') != -1 && keyName != '.'){
        //如果有.则拆开
        let keys = keyName.split('.')
        //设置临时变量,一层一层找下去
        let temp = dataObj;
        for(let i = 0;i<keys.length;i++){
            temp = temp[keys[i]]
        }
        return temp
    }
    //如果没有点
    return dataObj[keyName]
}

这个方法还是面试算法题之一哦

新建一个parseArray.js处理数组,结合renderTemplate实现递归。注意这个函数接收的是token,不是tokens

//token ['#',`xxx`,[]]
//递归调用的次数由数组‘xxx’的长度决定
import renderTemplate from './renderTemplate.js'
import lookup from './lookup.js'
export default function parseArray(token,data){
    //先得到整体data中要使用的数组
    let v = lookup(data,token[1])
    //结果字符串
    let resStr = ''
    //遍历v数组,v一定是数组
    //下面的循环十分重要
    for(let i = 0;i<v.length;i++){
        /****这里也很关键*****/
        resStr +=  renderTemplate(token[2],{
            //现在在v[i]的基础上补充一个.属性,完美的解决了{{.}}的情况
            '.':v[i],
            ...v[i]
        })
    }
    return resStr
}

import lookup from './lookup.js'
export default function parseArray(token,data){
    //先得到整体data中要使用的数组
    let v = lookup(data,token[1])
    //结果字符串
    let resStr = ''
    //遍历v数组,v一定是数组
    //下面的循环十分重要
    for(let i = 0;i<v.length;i++){
        /****这里也很关键*****/
        resStr +=  renderTemplate(token[2],{
            //现在在v[i]的基础上补充一个.属性,完美的解决了{{.}}的情况
            '.':v[i],
            ...v[i]
        })
    }
    return resStr
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

PrototypeONE

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值