Vue 源码学习 —— Mustache 模板引擎原理

前言

  • Mustache 是模板引擎思想的奠基者,Vue 中的模板引擎也借用了它的思想
  • 因此,在学习Vue的模板引擎之前,先学习Mustache能加更容易理解模板引擎的设计思想

Mustache

  • 下载:到 BootCDN 搜索 mustache,复制链接到浏览器打开,并保存到本地即可
  • 或使用该链接:mustache.js(v4.1.0)
  • 引入
    在这里插入图片描述
    在这里插入图片描述
  • 基本使用
  • 渲染普通对象:
 <div id="root"></div>
 <script src="js/mustache.js"></script>
 <script>
     let template = `
         <ul>
             <li>
                 <p>{{name}}的信息</p>
                 <p>姓名:{{name}}</p>
                 <p>年龄:{{age}}</p>
                 <p>性别:{{sex}}</p>
             </li>
         </ul>
     `
     let data = {
         name: 'aa',
         age: 12,
         sex: '女'
     }
     
     let DOMStr = Mustache.render(template, data)

     let root = document.getElementById('root')
     root.innerHTML = DOMStr
 </script>

在这里插入图片描述

  • 渲染普通数组:
 <div id="root"></div>
 <script src="js/mustache.js"></script>
 <script>
     let template = `
         <ul>
             {{#arr}}
                 <li>
                     {{.}}
                 </li>
             {{/arr}}
         </ul>
     `
     let data = {
         arr: ['A', 'B', 'C']
     }

     let DOMStr= Mustache.render(template, data)

     let root = document.getElementById('root')
     root.innerHTML = DOMStr
 </script>

在这里插入图片描述

  • 渲染对象数组:
 <div id="root"></div>
 <script src="js/mustache.js"></script>
 <script>
     let template = `
         <ul>
             {{#arr}}
                 <li>
                     <p>{{name}}的信息</p>
                     <p>姓名:{{name}}</p>
                     <p>年龄:{{age}}</p>
                     <p>性别:{{sex}}</p>
                 </li>
             {{/arr}}
         </ul>
     `
     let data = {
         arr: [
             {
                 name: 'aa',
                 age: 12,
                 sex: '女'
             },
             {
                 name: 'bb',
                 age: 16,
                 sex: '男'
             },
             {
                 name: 'cc',
                 age: 17,
                 sex: '男'
             },
         ]
     }
     let DOMStr= Mustache.render(template, data)
     
     let root = document.getElementById('root')
     root.innerHTML = DOMStr
 </script>

在这里插入图片描述

  • 渲染复杂嵌套情况:
 <div id="root"></div>
 <script src="js/mustache.js"></script>
 <script>
     let template = `
         <ul>
             {{#arr}}
                 <li>
                     {{name}}的爱好是:
                     <ol>
                         {{#hobbies}}
                             <li>{{.}}</li>
                         {{/hobbies}}
                     </ol>
                 </li>
             {{/arr}}
         </ul>
     `
     let data = {
         arr: [
             {name: 'aa', age: 12, hobbies: ['swim', 'music']},
             {name: 'bb', age: 16, hobbies: ['run', 'animation']},
             {name: 'cc', age: 17, hobbies: ['joggy', 'movie']}
         ]
     }

     let templateStr = Mustache.render(template, data)

     let root = document.getElementById('root')
     root.innerHTML = templateStr
 </script>

在这里插入图片描述

  • 冷知识:在没有ES6模板语法之前,可以这样去写模版 (相比字符串拼接更优雅)
 <div id="root"></div>
 <script src="js/mustache.js"></script>
 <!-- type 可以为任意值,只要不是text/javascript,浏览器无法识别就不会执行,也不会显示 -->
 <!-- 使用 id 是为了方便获取内部内容 -->
 <!-- 这样的好处还有高亮显示、语法补全 -->
 <script type="text/template" id="template">
     <div>
         {{#show}}
             <p>show</p>
         {{/show}}
         {{#hidden}}
             <p>hidden</p>
         {{/hidden}}
     </div>
 </script>
 <script>
     let template = document.getElementById('template').innerText

     let data = {
         show: true,
         hidden: false
     }

     let templateStr = Mustache.render(template, data)

     let root = document.getElementById('root')
     root.innerHTML = templateStr
 </script>

Mustache 原理

  • Mustache 会将模版字符串先编译成 tokens,然后再结合数据解析成 DOM 字符串
    在这里插入图片描述
  • tokens 是一个嵌套的 js 数组,也是抽象语法树 AST
    在这里插入图片描述
  • 当模版存在循环时:
    在这里插入图片描述
  • 模板存在双重循环时:
    在这里插入图片描述

手写简单源码

  • 目录结构
    在这里插入图片描述
  • index.js
import parseTemplateToTokens from './parseTemplateToTokens'
import renderTemplate from './renderTemplate'
window.Mustache = {
    render(templateStr, data) {
        let tokens = parseTemplateToTokens(templateStr)
        let DOMStr = renderTemplate(tokens, data)
        console.log(DOMStr)
    }
}

// 测试用例
let templateStr = `
    <div>
        {{#arr}}
            <p>{{name}}</p>
            <p>{{age}}</p>
            <p>{{sex}}</p>
            <p>爱好:</p>
            <ul>
                {{#fav}}
                    <li>{{.}}</li>
                {{/fav}}
            </ul>
        {{/arr}}
    </div>
`

let data = {
    arr: [
        {
            name: 'aa',
            age: 12,
            sex: '女',
            fav: ['movie', 'skr']
        },
        {
            name: 'bb',
            age: 16,
            sex: '男',
            fav: ['study', 'icecream']
        },
        {
            name: 'cc',
            age: 17,
            sex: '男',
            fav: ['swim', 'sing']
        }
    ]
}

window.Mustache.render(templateStr, data)
  • parseTemplateToTokens.js
import Scanner from './Scanner'
import nestTokens from './nestTokens'

export default function parseTemplateToTokens (templateStr) {
    let tokens = []
    let scanner = new Scanner(templateStr)
    let words
	// 扫描未结束
    while (!scanner.eos()) {
    	// 扫描并返回遇到 '{{' 之前的字符
        words = scanner.scanUntil('{{')
        if (words) {
        	// 存入 tokens,并且 '{{' 之前的字符为普通文本 即 text
            tokens.push(['text', words])
        }
        // 跳过 '{{'
        scanner.scan('{{')
        // 扫描并返回遇到 '}}' 之前的字符
        words = scanner.scanUntil('}}')
        // '{{' 和 '}}' 之间的字符为约定的语法格式,需要进行类型判断
        if (words) {
            if (words.startsWith('#')) {	// 循环开始标记
                tokens.push(['#', words.substring(1)])

            } else if (words.startsWith('/')) {	// 循环结束标记
                tokens.push(['/', words.substring(1)])

            } else {	// 变量标记
                tokens.push(['name', words])
            }
        }
        // 跳过 '}}'
        scanner.scan('}}')
    }
    // 将 tokens 形成嵌套的数组结构
    tokens = nestTokens(tokens)

    return tokens
}
  • Scanner.js
export default class Scanner {
    constructor (templateStr) {
        this.templateStr = templateStr
        this.tail = templateStr
        this.pos = 0
    }
	// 扫描并跳过
    scan(tag) {
        if (this.tail.indexOf(tag) == 0) {
            this.pos += tag.length
            this.tail = this.templateStr.substring(this.pos)
        }
    }
	// 扫描并返回
    scanUntil (stopTag) {
        const pos_backup = this.pos
        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
    }
}
  • nestTokens.js
// 利用 #、/ 作为入栈和出栈的标志,嵌套tokens
export default function nestTokens (tokens) {
    let nestedTokens = []
    let stack = []
    stack.push(nestedTokens)
    for (let i=0; i<tokens.length; i++) {
        let token = tokens[i]

        switch (token[0]) {
            case '#':
                token[2] = []
                stack[stack.length-1].push(token)
                stack.push(token[2])

                break
            case '/':
                stack.pop()
                break
            default:
                stack[stack.length-1].push(token)
        }
    }
    return nestedTokens
}
  • renderTemplate.js
// 递归的将 tokens 转换成 DOMStr
export default function renderTemplate (tokens, data) {
    let DOMStr = ''
    for (let i=0; i<tokens.length; i++) {
        let token = tokens[i]
        switch (token[0]) {
            case 'text':
                DOMStr += token[1]
                break
            case 'name':
                token[1] === '.' 
                    ? DOMStr += data
                    : DOMStr += data[token[1]]
                break
            case '#':
                for (let i=0; i<data[token[1]].length; i++) {
                    let depData = data[token[1]][i]
                    DOMStr += renderTemplate(token[2], depData)
                } 
        }
    }
    return DOMStr
}
  • 结果
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

tanleiDD

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

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

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

打赏作者

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

抵扣说明:

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

余额充值