【vue源码解析之底层模板引擎】mustache模板引擎

什么是模板引擎?

模板引擎是将数据变为视图的最优雅的解决办法。目前将数据处理为视图的方法,从时间线排列有:

  • 纯DOM法 document.createElement()

  • 数组 join,借助 HTMLElement.prototype.innerHTML将字符串解析为HTML

  • es6 模板字符串 <h1>${data}</h1>替代 join 函数

  • 模板引擎,vue中的<li v-for="item in data"></li>就是一种模板引擎。此处解析的mustache是最早的模板引擎,因它的嵌入标记{{}}像胡子而命名

例如:将下图数据转为视图

// 数据
const data = {
    title: '信息',
    arr: [
        { name: 'Jay', age: 18},
        { name: 'Bin', age: 20}
    ],
}
<!-- 模板 -->
<p>{{title}}</p>
<ul>
    {{#arr}}
    <li>
        <p>{{name}}</p>
        <p>{{age}}</p>
    </li>
    {{/arr}}
</ul>
<!-- 视图 -->
<p>信息</p>
<ul>
    <li>
        <p>Jay</p>
        <p>18</p>
    </li>
    <li>
        <p>Bin</p>
        <p>20</p>
    </li>
</ul>

mustache 基本用法

引入mustache.4.1.0.js

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="box"></div>

  <script src="./mustache4_1_0.js"></script>
    
  <!-- text/template 不会被识别,也可让代码在ide着色 或用模板字符串 -->
  <script type="text/template" id="myTemplate">
    <p>{{title}}</p>
    <ul>
      {{#arr}}
        <li>
            <p>{{name}}</p>
            <p>{{age}}</p>
        </li>
      {{/arr}}
    </ul>
  </script>
    
  <script>
    // 数据
    const data = {
      title: '信息',
      arr: [
        { name: 'Jay', age: 18 },
        { name: 'Bin', age: 20 }
      ]
    }
    
    const templateStr = document.getElementById('myTemplate').innerHTML;
    const domStr = Mustache.render(templateStr, data)
    const box = document.getElementById('box')
    box.innerHTML = domStr

  </script>
</body>

</html>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F84tK392-1647530834255)(.\mustache模板引擎.assets\image-20220317212633628.png)]

mustache 原理

<p>{{title}}</p> 转为 <p>信息</p> 很简单

templateStr.replace(/\{\{(\w+)\}\}/, function (findStr, $1) {
    return data[$1];
})

但遇到复杂嵌套的数据怎么解决呢?mustache 引入了 token 的概念。在 mustache.js 中打断点输出 tokens,可以看到模板被解析为下图所示 tokens

在这里插入图片描述

它用 text 标记普通文本,用 name 标记嵌入数据名称,并形成和模板一样的嵌套结构

在这里插入图片描述

根据 tokens 和 data 形成最后的字符串,下面自己实现一下。

在这里插入图片描述

自定义mustache

目标实现双重嵌套

let templateStr = `
    <p>{{title}}</p>
    <ul>
      {{#arr}}
        <li>
            <p>{{name}}</p>
            <p>{{age}}</p>
            {{#num}}
            <span>{{.}}</span>
            {{/num}}
        </li>
      {{/arr}}
    </ul>`;
const data = {
    title: '信息',
    arr: [
        {name: 'Jay', age: 18, num: [1, 2, 3]},
        {name: 'Bin', age: 20, num: [1, 2, 3]}
    ],
}

1. parseTemplateStrToTokens 将模板转化为 tokens

第一步不是直接实现嵌套结构的 tokens,先将模板转为扁平的一维 tokens.

原理:先检索{{,将之前的数据用 text 标记,再在剩余数据检索}},中间数据用 name 标记,循环往复,直至结束。此处未考虑{{}}不对称及其他情况,只是简单实现。

export default function ParseTemplateStrToTokens(templateStr) {
    let stopScanUtilTag = '{{';
    let stopScanTag = '}}';

    let tokens = [];
    scanUtil(tokens, templateStr);
    return tokens;
    
    /* 收集 name 即{{}}中间的东西 */
    function scan(tokens, tail) {
        let res;
        let pos = tail.search(stopScanTag);
        if (pos > 0) {
            res = tail.substring(0, pos);
            tail = tail.substring(pos + stopScanTag.length);
        } else {
            res = tail;
        }
        res = res.trim();
        if (res.length > 0) {
            // 用 # / 标记嵌套开始和结束
            if (res[0] === '#' || res[0] === '/') {
                tokens.push([res[0], res.substring(1)])
            } else tokens.push(['name', res]);
        }
        if (pos > 0 && tail.length > 0) {
            scanUtil(tokens, tail);
        }
    }

    /* 收集 text */
    function scanUtil(tokens, tail) {
        let res;
        let pos = tail.search(stopScanUtilTag);
        if (pos > 0) {
            res = tail.substring(0, pos);
            tail = tail.substring(pos + stopScanUtilTag.length);
        } else {
            res = tail;
        }
        res = res.trim();
        if (res.length > 0) tokens.push(['text', res]);
        if (pos > 0 && tail.length > 0) {
            scan(tokens, tail);
        }
    }
}

输出 tokens

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OP6Zlmny-1647530834257)(.\mustache模板引擎.assets\image-20220317223042849.png)]

下面将一维的 tokens 转为嵌套结构。

思想:这里用 nestTokens 存储嵌套的 tokens,并用到数据结构栈 stack。

将 tokens 分为三类: #、/、default,进行遍历

  • # : 压栈
  • /: 退栈,直至遇到 # 且属性相同的 token,用 section 存储退栈的数据并存在当前 # token中,若此时栈为空,则 # token 压入 nestTokens,否则依旧压入栈 stack
  • default: 若栈为空,压入 nestTokens,否则压入 stack
function nestTokens() {
    let flag = 0;   // 记录栈是否为空
    let stack = [];
    let nestTokens = [];    // 最终嵌套tokens
    tokens.forEach(token => {
        switch (token[0]) {
            case '#':
                flag++;
                stack.push(token)
                break;
            case '/':
                flag--;
                let section = [];
                let temp;
                while (true) {
                    temp = stack.pop();
                    if (temp[0] === '#' && temp[1] === token[1]) break;
                    else section.unshift(temp);
                }
                temp[2] = section;
                if (flag === 0) nestTokens.push(temp);
                else stack.push(temp)
                break
            default:
                if (flag === 0) nestTokens.push(token);
                else stack.push(token)
        }
    })
    return nestTokens;
}

输出 nestTokens

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QL2cwcLl-1647530834257)(.\mustache模板引擎.assets\image-20220317225445116.png)]

2. renderTemplate 生成dom字符串

思想: 对 tokens 遍历,遇到嵌套结构,再针对数据遍历。递归调用。

const data = {
    title: '信息',
    arr: [
        {name: 'Jay', age: 18, num: [1, 2, 3]},
        {name: 'Bin', age: 20, num: [1, 2, 3]}
    ],
}

export default function renderTemplate(tokens, data) {
    let domStr = '';
    tokens.forEach(token => {
        if (token[0] === 'text') domStr += token[1];
        else if (token[0] === 'name') {
            if (token[1] !== '.') domStr += data[token[1]];
            else domStr += data;
        } else if (token[0] === '#') {
            data[token[1]].forEach(data => {
                domStr += renderTemplate(token[2], data);
            })
        }
    })
    return domStr;
}

输出 domStr(未改变格式)

<p>信息</p>
    <ul><li>
            <p>Jay</p>
            <p>18</p><span>1</span><span>2</span><span>3</span></li><li>
            <p>Bin</p>
            <p>20</p><span>1</span><span>2</span><span>3</span></li></ul>

3. index.js 整合挂载到全局

import ParseTemplateStrToTokens from "./parseTemplateStrToTokens"
import RenderTemplate from "./renderTemplate";

window.templateEngine = {
    render(templateStr, data) {
        let tokens = ParseTemplateStrToTokens(templateStr);
        let domStr = RenderTemplate(tokens, data)
        return domStr;
    }
}

使用

<body>
<div id="app"></div>

<script>
    let templateStr = `
    <p>{{title}}</p>
    <ul>
      {{#arr}}
        <li>
            <p>{{name}}</p>
            <p>{{age}}</p>
            {{#num}}
            <span>{{.}}</span>
            {{/num}}
        </li>
      {{/arr}}
    </ul>`;
    const data = {
        title: '信息',
        arr: [
            {name: 'Jay', age: 18, num: [1, 2, 3]},
            {name: 'Bin', age: 20, num: [1, 2, 3]}
        ],
    }
    let domStr = templateEngine.render(templateStr, data);
    document.getElementById('app').innerHTML = domStr;
</script>


</body>

结果

至此,自定义实现了 mustache 简易功能,了解了vue底层模板引擎实现的思路。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值