Vue2源码解析(3)——生成ats树

一:前言

        在Vue2中,无论是render()函数,还是template模板,Vue2都是先去解析模板参数,然后生成一颗ats树,然后ats树再去生成一串字符串,最终将这棵树变成一个字符串,也就是我们所说的虚拟DOM,而后才是转换为真实DOM,在本文将会为各位小伙伴讲解一下生成ats树的详细步骤。

        本文是基于(2)来接着写的,有时间的小伙伴可以看一下(2)哦,当然不看也不会有太大的影响。链接如下

Vue2源码解析(2)——响应式详解-CSDN博客vue2的响应式源码https://blog.csdn.net/c18559787293/article/details/133454673?spm=1001.2014.3001.5502

二:生成ats树的源码

1、项目目录

        以下是项目的目录,由于是基于(1)所续写的,这里我们新增的文件主要是dist下的2解析模板参数.index.html文件和 compiler文件夹下的两个文件,index.js是转换为语法树的入口文件,而parse.js是封装的详细方法。

2、HTML文件代码

        以下是HTML的代码,在这里我们采用的是el模板,通过vm.$mount挂载了一个id为app的div标签。这里的写法也可以用render函数等,最终在生成ats树的方法里都会进行处理。

<!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="app">
        <div style="color: red;">{{name}}</div>
        <span>{{age}}</span>
    </div>
    <script src="vue.js"></script>
    <script>
        const vm = new Vue({
            data: {
                name: 'zf',
                age: 20,
                address: {
                    content: '回龙观',
                    num: 30
                },
                hobby: ['eat', 'drink']
            },
            // el: '#app' // 我们要将数据解析到el元素上
            // template:'<div>hello</div>' 
        })
        vm.$mount('#app')
        // 1. 模板引擎 性能很差,使用正则匹配进行替换,因为1.0版本的时候没有虚拟DOM
        // 2.采用虚拟DOM,数据变化后比较虚拟DOM的差异,最后更新需要更新的地方
        // 3.核心就是将模板变成我们的JS语法,通过JS生成虚拟DOM
    
        // 从一个东西变成另一个东西,比如语法之间的转化,从es6 ==> es5
        // CSS压缩,先变成语法树,再重新组装成新的语法  将template语法转换成render函数

    </script>
</body>

</html>

 3、complier文件夹下的index.js文件

        这个文件其实没什么可说的,注释很清楚,主要就是封装生成树的方法,因为后续还会转换为虚拟DOM等,所以拆开来各种不同的文件便于观看。

import { parseHTML } from "./parse"


export function compileToFunction(template) {
    // 1. 将template 转化成ast语法树
    let ast = parseHTML(template)
    // 2. 生成render方法(render方法执行后的返回的结果就是虚拟DOM)
    console.log(ast)
}

4、compiler文件夹下的parse.js文件 

        该文件是生成树的方法,首先上面是定义的的七个常量,使用正则表达式进行不同的匹配。在这个文件里,首先进入的是parseHTML()方法,由于在Vue2中,要求模板必须是“开始标签”+“内容”+“结束标签”,所以我们就匹配开始标签的“<”,看位置是否是0就好了。

        注意:Vue3没有这个要求,可以直接写内容不需要开始标签。因此在Vue3中时一个字符一个字符去匹配的。

        在这个方法里,我们定义了一个具有父亲和孩子的结点,进行循环传进来的html模板,匹配“<”的位置,根据位置的不同去执行不同的onXXX()方法,然后对节点进行不同的判断与操作。

        同时对于结点的处理, 我们采用的是栈的后进先出策略,去进行匹配弹出对应的开始和结束标签,当栈为空之后,则匹配完成。

//解析模板文件
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 这里匹配到的是一个标签名,<xxx 匹配到的是开始标签的名字
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 这是一个结束标签,返回的是一个名字
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 这是匹配的一个属性
// ↑ 第一个分组是key,value是 分组3/分组4/分组5
const startTagClose = /^\s*(\/?)>/; // 这里匹配的是单标签
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // {{name}} 匹配的内容就是表达式的变量

// Vue3采用的不是正则,而是一个字符一个字符来判断
// 对模板进行编译处理
export function parseHTML(html) { // Vue2的 开头肯定是一个 <

    const ELEMENT_TYPE = 1; // 元素类型是1
    const TEXT_TYPE = 3; // 文本类型是3
    const stack = [] // 用于存放元素的栈
    let currentParent; // 指向栈中的最后一个
    let root; // 根节点

    function createASTElement(tag, attrs) { // 创建ast节点元素
        return {
            tag,
            type: ELEMENT_TYPE,
            children: [], // 孩子
            attrs, // 属性
            parent: null
        }
    }

    // 下面这三个是暴露出来的方法,最终需要转换成一颗抽象语法树
    // 利用栈型结构 来构造一棵树
    function onStart(tag, attrs) {
        // console.log(tag,attrs,'开始标签')
        let node = createASTElement(tag, attrs)
        if (!root) { // 判断如果没有根节点,那么就把这个node给根节点,作为树根
            root = node
        }
        if (currentParent) { // 如果当前有currentParent节点,那么就把这个节点的父节点设置成当前的currentParent
            node.parent = currentParent
            currentParent.children.push(node) // 给父亲的儿子赋值
        }
        stack.push(node); // 把这个节点丢进栈里面
        currentParent = node; // currentParent为栈中的最后一个
    }

    function onText(text) { // 遇到文本后,直接放进当前指向currentParent的孩子里
        // console.log(text,'文本')
        text = text.replace(/\s/g,'')
        text && currentParent.children.push({ // 文本直接放到当前指向的节点中
            type: TEXT_TYPE, // 这个类型是文本
            text, // 文本的内容
            parent: currentParent
        })
    }

    function onEnd(tag) { // 是结束标签的时候要把栈弹出来,并且更新指向节点
        // console.log(tag,'结束标签')
        let node = stack.pop(); // 弹出最后一个,校验标签是否合法
        currentParent = stack[stack.length - 1]
    }

    // 前进,删除掉已经匹配的字符串
    function advance(n) {
        html = html.substring(n);
    }

    // 开始标签的匹配
    function parseStartTag() {
        // 匹配开始标签,标签名 之后匹配属性,删掉标签结束
        const start = html.match(startTagOpen);
        if (start) {
            const match = {
                tagName: start[1], // 标签名
                attrs: [] // 属性
            }
            advance(start[0].length) // 匹配完成后,截取掉这段,然后再进行匹配
            let attr, end
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                advance(attr[0].length)
                match.attrs.push({ name: attr[1], value: attr[3] || attr[5] || attr[5] || true })
            }
            if (end) {
                advance(end[0].length)
            }
            return match
        }

        // 如果不是开始标签的结束,就一直匹配下去
        // console.log(html)
        return false; //不是开始标签
    }
    while (html) {
        // 如果textEnd 为0 ,则说明这是一个开始标签
        // 如果textEnd >0 ,则说明这是一个结束标签
        let textEnd = html.indexOf('<'); // 如果indexOf中的索引是0,则说说明是个标签

        if (textEnd == 0) { // 当是一个开始标签的时候
            const startTagMatch = parseStartTag(); // 开始标签的匹配结果

            if (startTagMatch) {// 解析到了开始标签 跳过本轮循环,因为已经是开始标签了
                onStart(startTagMatch.tagName, startTagMatch.attrs)
                continue
            }
            // 这里是第三个执行了,先是上面的开始标签,再是下面的内容,再是结束标签。 如果不是开始标签,那就是结束标签
            let endTagMatch = html.match(endTag);
            if (endTagMatch) {//匹配结束标签,如果有值,就删掉
                advance(endTagMatch[0].length);
                onEnd(endTagMatch[1])
                continue
            }
        }
        if (textEnd > 0) { // 截取文本,这里大于0 则是结束标签了,所以截取0到textEnd,就是内容了
            let text = html.substring(0, textEnd); // 文本内容
            if (text) {
                onText(text)
                advance(text.length); // 解析到的文本
            }
        }
    }
    // console.log(root)
    return root
}

 5、运行效果如下

        生成的效果图可以看到开始 

        展开后的详细内容,在下图中我们可以看到,这是一个id='app'的div标签,下面有两个子标签,分别是style='color:red;',并且内容为{{name}}的div标签,以及内容为{{age}}的span标签,对比上文HTML的代码,生成的是符合模板的,因此ast树生成成功。

 三:总结

         ast树是模板想要转换为真实DOM的第一步,其实基本上是使用正则进行不断地匹配,然后根据匹配的结果进行相对应的处理。总体实现来讲不算很难,逻辑也不会太过于复杂。多注意一些细节上的处理就可以了。好啦,本文就讲到这里,希望能对各位小伙伴有所帮助哦!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

暴怒的代码

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

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

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

打赏作者

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

抵扣说明:

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

余额充值