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 += [
        '
  • ' '

    '+arr[i].name+'/p>' '

  • '
    ].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 += `
        	
  • ${arr[i].name}

  • `
    }

    mustache的基本使用

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

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

    循环对象数组

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

    let str = `
        {{#arr}}
            
  • {{name}}/li> {{/arr}}

    {{time}}

    ` 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> {{/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}}
            
  • {{name}}/li> {{#hobbies}} {{.}} {{/hobbies}} {{/arr}} ` Mustache.render(str,data)
  • data也可以传入布尔值,效果类似v-if

    const data = {
        boolean:false
    }
    let str = `
        
    {{#bollean}}

    xxx

    {{/bollean}}
    `
    Mustache.render(str,data)

    解决反引号繁杂的问题

    
    
    

    mustache的底层核心机理

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

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

    let templateStr = `

    我买了一个{{thing}},好{{mood}}

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

    当较为复杂时不能使用,较为复杂时采用以下机制
    Vue源码学习之mustache模板引擎_第1张图片
    )]

    • tokens是一个js的嵌套数组,说白了就是模板字符串的js表示
    • 它是“抽象语法树”、“虚拟节点”等等的开山鼻祖
    //模板字符串
    let str = `

    我买了一个{{thing}},好{{mood}}

    `
    //经过编译 //tokens let token = [ ["text","

    我买了一个"], ["name","thing"], ["text","好"]["name","mood"] ["text","啊

    "
    ] ]

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

    [
        ["text","
      "], ["#","arr",[ ["text","
    • "] ["name","."] ["text","
    • "
      ] ]], ["text","
    "
    ] ]

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

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

    手写实现mustache库

    使用webpack和webpack-dev-serve构建

    • 手写Scanner类
    • 手写html=>tokens
    • 手写将tokens=>注入数据
    <script>
        let tempalteStr = `

    我买了一个{{thing}},好{{mood}}

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

    你可能感兴趣的:(Vue源码学习,vue.js,学习,javascript)