Vue源码探索--mustache模板引擎(把模板字符串编译为虚拟DOM并渲染出真实DOM)上篇

本文介绍如何使用正则表达式实现简单的模板数据填充,并深入探讨mustache库的工作原理,包括模板字符串编译成tokens的过程及tokens结合数据生成DOM字符串的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、正则实现简单模板数据填充:

二、mustache库的实现机理:

1. 过程解析:

  (1) tokens解释:

(2)过程图示:

(3)mustache库底层重点要做两个事情:

三、实现底层原理:

1、代码目录:

2、实现模板字符串编译为tokens的过程:

  (1)   将模板字符串提取出来(Scanner.js):

(2)将模板字符串变成tokens数组(parseTemplateToTokens.js):

(3)将零散的tokens数组嵌套起来(nestTokens.js):


注:上一基础篇我们了解到什么是模板引擎以及如何使用,这一篇我们通过尝试写一些底层源码来深入理解其工作流程(把模板字符串编译为虚拟DOM),下一篇中我们在讲解如何结合数据渲染出真实DOM。

一、正则实现简单模板数据填充:

最简单模板引擎实现机理,利用的是正则表达式中的replace()方法:

<div id="container">

</div>
</body>
<script src="../js/mustache.js"></script>
<script>
    var templateStr = `
        <h1>我买了一棵{{thing}},花了{{money}}元,好{{mood}}</h1>
    `;

    var data = {
        thing:"白菜",
        money:5,
        mood:"激动"
    };


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

    var domStr = render(templateStr,data);
    var dom = document.getElementById("container");
    dom.innerHTML = domStr;
    console.log(domStr);
</script>

代码解释:
    replace()的第一个参数是正则表达式,第二个参数可以是一个函数。函数可以接受参数:第一个参数是findStr 是你查找的这部分内容 {{thing}}、{{money}}、{{mood}},第二个参数是 $1 你捕获到的内容 thing、money、mood。
   

但是正则只实现简单的模板引擎替换,如果遇到复杂的还是需要我们使用 mustache 来实现。
    

二、mustache库的实现机理:


1. 过程解析:

首先是模板字符串经过编译变为tokens,这一过程我们称为模板字符串变为虚拟DOM的过程。接着虚拟DOM结合数据进行解析成为真实DOM,这一过程我们称为虚拟DOM转换为真实DOM的过程。

 (1) tokens解释:

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

 将模板字符串转换为JS的嵌套数组,tokens相当于一个二维数组里面存放的是一个一个的token。在每一个token中文字和标签(在模板字符串中没有处理意义)是由"text"表示,而带双大括号的变成name。

(2)过程图示:

 


 当模板字符串嵌套层级更深的时候即有循环的存在,它将编译为嵌套更深的tokens。

可以看到 tokens[1]中有数组的嵌套。

 


(3)mustache库底层重点要做两个事情:

  • 将模板字符串编译为tokens形式(将真实DOM变为虚拟DOM)。

注:虚拟DOM变为真实DOM的内容是在diff算法中涵盖的。(后边的博客我们有讲解到)

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

三、实现底层原理:

1、代码目录:

底层实现的环境是webpack、ES6。

2、实现模板字符串编译为tokens的过程:

(1) 将模板字符串提取出来(Scanner.js):

创建一个Scanner.js实现对,模板字符串的扫描过程。Scanner.js里面有两个函数一个是scan方法,一个是scanUntil方法。

scan:方法功能弱,就是走过指定内容(大括号),没有返回值。即跳过大括号

    //功能弱,就是走过指定内容,没有返回值
    scan(tag) {
        if (this.tail.indexOf(tag) == 0) {
            //    tag有多长,比如{{长度是2,就让指针后移多少位
            this.pos += tag.length;
            //    尾巴也要变
            this.tail = this.templateStr.substring(this.pos);
        }
    }

scanUntil:让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字。即找到大括号并将大括号之前和之后的文字收集并返回。

    //让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUntil(stopTag) {
        //记录一下执行本方法的时候pos的值
        const pos_backUp = this.pos;
        //当尾巴的开头不是它 (定义一个尾巴当做收集当前指针所在位置到字符串最后的字符)
        while (this.tail.indexOf(stopTag) != 0 && this.pos < templateStr.length) {
            this.pos++;
            //改变尾巴 从当前指针这个字符开始 到最后的全部字符
            this.tail = this.templateStr.substr(this.pos);
        }
        return this.templateStr.substring(pos_backUp, this.pos);
    }

Scanner.js

export default class Scanner {
    
    constructor(templateStr) {

        console.log(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.tail = this.templateStr.substring(this.pos);
        }
    }
    //让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
    scanUntil(stopTag) {
        //记录一下执行本方法的时候pos的值
        const pos_backUp = this.pos;
        //当尾巴的开头不是它 (定义一个尾巴当做收集当前指针所在位置到字符串最后的字符)
        while (this.tail.indexOf(stopTag) != 0 && this.pos < templateStr.length) {
            this.pos++;
            //改变尾巴 从当前指针这个字符开始 到最后的全部字符
            this.tail = this.templateStr.substr(this.pos);
        }
        return this.templateStr.substring(pos_backUp, this.pos);
    }
}

过程解析:

我们首先定义一个头指针pos和一个尾巴指针tail,头指针为0,尾巴指针刚开始为整个模板字符串。

scanUntil的方法就是返回"{{" 和"}}"之间的字符串,而scan方法就是跳过大括号"{{" " }}"。

scanUntil方法中我们用一个pos_backUp来记录头指针然后在while循环里面不断地改变pos的值,尾巴指针也在跟着改变,判断的条件是尾巴指针的第一个字符不是stopTag,并且pos指针小于模板字符串的长度。

scan方法则是根据tag改变pos指针,使pos指针加上tag的长度同时尾巴也跟着改变,跳过"{{"和"}}"。

(2)将模板字符串变成tokens数组(parseTemplateToTokens.js):

 在parseTemplateToTokens.js中我们创建一个扫描器,在while循环中我们不断地收集scanUntil经过的字符并存起来和跳过scan经过的字符。重点在与tokens二维数组的形成。

import Scanner from './Scanner.js'
import nestTokens from "./nestTokens"
/*
 *将模板字符串变为tokens数组
 * */

export default function parseTemplateToTokens(templateStr) {
    var tokens = [];
    //    创建扫描器
    var scanner = new Scanner(templateStr);
    var words;
    //    让扫描器工作
    while (scanner.pos != templateStr.length) {
        //收集
        words = scanner.scanUntil("{{");
        //存起来
        if (words != "") {
            tokens.push(['text', words]);
        }
        //过双大括号
        scanner.scan("{{");
        //收集之前出现的文字
        words = scanner.scanUntil("}}");
        //存起来
        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("}}");
    }
    console.log(tokens);
    return nestTokens(tokens);
}

这段代码是判断 {{ xxxx}}双大括号中间的内容,判断一下首字符。需要把“#”和“/”当做tokens数组里面子数组的第一个下标为0的元素。

        //存起来
        if (words != "") {
            //判断一下首字符
            if (words[0] == '#') {
                //存起来,从下标为1的项开始存,因为下标为0的项是#
                tokens.push(['#', words.substring(1)]);
            } else if (words[0] == '/') {
                //存起来,从下标为1的项开始存,因为下标为0的项是/
                tokens.push(['/', words.substring(1)]);
            } else {
                //    存起来
                tokens.push(['name', words]);
            }

        }

此时打印出来的内容相当于如下:(#和/之间的内容并没有嵌套起来)

 

(3)将零散的tokens数组嵌套起来(nestTokens.js):

就是把#和/之间的内容用数组装起来,这里我们运用的是栈的思想(先进后出/后进先出)我们遇见#就压栈,遇见/就出栈,#和/之间的内容都是其子元素。

新建一个nestTokens.js进行进栈压栈的操作:

定义结果数组:nestedTokens,收集器:collector,接着遍历传进来的tokens通过switch判断token的第0项是什么(有三种情况一种是“#”,一种是“/”,一种什么也不是)。

/*
 * 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的选项
 * */
export default function nestTokens(tokens) {
    //    结果数组
    var nestedTokens = [];
    //栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)
    //的tokens数组当前操作的这个tokens小数组
    var sections = [];
    //收集器指向结果数组,引用类型值,所以指向的是同一个数组
    var collector = nestedTokens;
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i];

        switch (token[0]) {
            case '#':
                //        将收集器放入到这个token
                collector.push(token);
                //        压栈从队尾进
                sections.push(token);
                //       更新收集器的状态,指向数组中的嵌套数组
                collector = token[2] = [];
                break;
            case '/':
                //出栈,pop()会返回刚刚弹出的项
                sections.pop();
                //    改变收集器为栈结构的上一层(队尾(队尾是栈顶))
                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
            default:
                collector.push(token);
        }
    }
    return nestedTokens;
}
  1. 在switch中case为"#"时,我们将这个token数组放入到收集器中并且进行压栈,重要是更改收集器的状态。
  2. case为"/"时首先要出栈并改变收集器的状态(此时要判断sections数组的长度)
  3. 一般情况则是正常压栈

重点在于:收集器(collector)是引用类型值,它和结果数组的内存地址是一样的,所以改变收集器的值,结果数组的值会改变。

经过上面的步骤我们可以看到,已经成功将模板字符串变为虚拟DOM也就是完成了一半,下一篇中我们将会讲解如何结合数据渲染出真实DOM。

 

注:代码地址

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值