Vue源码探秘之AST抽象语法树

前言

  突然发现草稿箱里还躺着这篇早在很早之前就完成的文章,抓紧发了,内容是之前编辑的,如有错误,麻烦告知。😁

一、抽象语法树是什么

  在开发Vue的时候,我们要使用Vue提供的模板语法,包括我们会使用一些指令,比如 v-for v-if 等等,也会使用双大括号 {{}} 这样的模板标记,Vue的底层会帮我们把它编译成正常的HTML语法,但是直接编译非常困难,所以人们会借助抽象语法树AST进行周转,让编译工作变得简单。

  也就是说,模板语法不是直接变成正常的HTML语法的,而是通过先变成抽象语法树AST,然后再把AST变成正常的HTML语法,抽象语法树AST起到一个中间过渡的作用。
在这里插入图片描述
在这里插入图片描述

  • 抽象语法树本质上就是一个JS对象
      例如Vue会将下图彩色部分代码以字符串的视角审视,然后解析为AST。
    在这里插入图片描述

  • 抽象语法树和虚拟节点的关系
      模板语法会变成抽象语法树AST,而抽象语法树最后不会变成虚拟节点,而是会直接变成渲染函数(h函数),h函数的执行会变成虚拟节点,虚拟节点经过diff 会在界面上显示。也就是说抽样语法树是不会 diff 的,diff 是用虚拟节点 diff 的,抽象语法树也不是直接生成 虚拟节点 的。关于 Vue源码探秘之虚拟节点和DIFF算法 可以看我的另一篇博客。
    在这里插入图片描述

二、相关算法储备 - 指针思想

  指针就是下标,不是C语言中的指针。

  C语言中的指针可以操作内存,JS中的指针就是一个下标位置。

  例如下题:

试寻找字符串中,连续重复次数最多的字符。
‘aaaabbbbbcccccccccccccdddddd’

  思路:初始 i:0 ,j:1 ,如果i和j指向的字一样,那么 i 不动,j 后移;如果 i 和 j 指向的字不一样,此时说明它们之间的字都是连续相同的,让 i = j, j 后移。

  js 代码:

// 试寻找字符串中,连续重复次数最多的字符。
var str = 'aaaabbbbbcccccccccccccdddddd';

// 指针
var i = 0;
var j = 1;

// 当前重复次数最多的次数
var maxRepeatCount = 0;

// 重复次数最多的字符串
var maxRepeatChar = '';

// 当i还在范围内的时候,应该继续寻找
while (i <= str.length - 1) {
	// 看i指向的字符和j指向的字符是不是不相同
	if (str[i] != str[j]) {
		// console.log(i + '和' + j + '之间的文字连续相同!都是字母' + str[i] + '它重复了' + (j - i) + '次');
		// 和当前重复次数最多的进行比较
		if (j - i > maxRepeatCount) {
			// 如果当前文字重复次数(j - i)超过了此时的最大值,就让它成为最大值
			maxRepeatCount = j - i;
			// 将i指针指向的字符存为maxRepeatChar
			maxRepeatChar = str[i];
		}
		// 让指针i追上指针j
		i = j;
	}
	// 不管相不相同,j永远要后移
	j++;
}

// 循环结束之后,输出答案
console.log(maxRepeatChar + '重复了' + maxRepeatCount + '次,是最多的连续重复字符');

在这里插入图片描述

三、相关算法储备 - 递归深入

1、递归题目1

  试输出斐波那契数列的前10项,即1、1、2、3、5、8、13、21、34、55

  在看该视频之前,我都是这样写的:

// 创建一个函数,功能是返回下标为n的这项的数字
function fib(n) {
    // 看下标n是不是0或者是不是1,如果是,就返回常数1
    // 如果不是,就递归
    return n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2);;
}

for (let i = 0; i <= 9; i++) {
    console.log(fib(i));
}

在这里插入图片描述

  看似没什么问题,但是却有大量的重复运算,我们 console.count() 进行计数看看:
在这里插入图片描述
  共计数 276 次,
在这里插入图片描述
  这时我们可以采用 cache思想,即用一个对象,缓存记录下标 i = 0 时 为 1,i = 1 时为 1,等等。判断缓存对象中有没有这个值,如果有,直接用,如果没有,写入缓存。也就是说,每算一个值,就要把这个值存入缓存对象。

// 缓存对象
var cache = {};

// 创建一个函数,功能是返回下标为n的这项的数字
function fib(n) {
    // 判断缓存对象中有没有这个值,如果有,直接用
    if (cache.hasOwnProperty(n)) {
        return cache[n];
    }
    // 缓存对象没有这个值
    // 看下标n是不是0或者是不是1,如果是,就返回常数1
    // 如果不是,就递归
    var v = n == 0 || n == 1 ? 1 : fib(n - 1) + fib(n - 2);
    // 写入缓存。也就是说,每算一个值,就要把这个值存入缓存对象。
    cache[n] = v;
    return v;
}

for (let i = 0; i <= 9; i++) {
    console.log(fib(i));
}

在这里插入图片描述

  可以看到运行结果相同。然后我们进行计数,可以看到共计数26次。即fib()函数被触发26次,明显少了。
在这里插入图片描述

2、递归题目2

  形式转换:试将高维数组[1, 2, [3, [4, 5], 6], 7, [8], 9]变为下图中所示的对象:
在这里插入图片描述

(1) 转换函数写法1
// 测试数组
var arr = [1, 2, [3, [4, 5], 6], 7, [8], 9];

function convert(arr) {
    // 准备一个结果数组
    var result = [];
    // 遍历传入的arr的每一项
    for (let i = 0; i < arr.length; i++) {
        // 如果遍历到的数字是number,直接放进入
        if (typeof arr[i] == 'number') {
            result.push({
                value: arr[i]
            });
        } else if (Array.isArray(arr[i])) {
            // 如果遍历到的这项是数组,那么就递归
            result.push({
                children: convert(arr[i])
            });
        }
    }
    return result;
}

var o = convert(arr);
console.log(o);

在这里插入图片描述

(2) 转换函数写法2

  参数不是arr这个词语,而是item,意味着现在item可能是数组,也可能是数字。

  写法1的递归次数要大大小于写法2,因为写法2中遇见什么都要递归一下。

// 测试数组
var arr = [1, 2, [3, [4, 5], 6], 7, [8], 9];

function convert(item) {
    if (typeof item == 'number') {
        // 如果传进来的参数是数字
        return {
            value: item
        };
    } else if (Array.isArray(item)) {
        // 如果传进来的参数是数组
        return {
            children: item.map(_item => convert(_item))
        };
    }
}

var o = convert(arr);
console.log(o);

在这里插入图片描述

四、相关算法储备 - 栈

1、栈相关知识
  • 栈(stack)又名堆栈,它是一种运算受限的线性表,仅在表尾能进行插入和删除操作。这一端被称为栈顶,相对地,把另一端称为栈底。
  • 向一个栈插入新元素又称作进栈、入栈或压栈;从一个栈删除元素 又称作出栈或退栈。
  • 后进先出(LIFO)特点:栈中的元素,最先进栈的必定是最后出栈, 后进栈的一定会先出栈。
  • JavaScript中,栈可以用数组模拟。需要限制只能使用push()和pop(),不能 使用unshift()和shift()。即,数组尾是栈顶。
  • 当然,可以用面向对象等手段,将栈封装的更好。
    在这里插入图片描述
2、利用“栈”的题目:

  试编写“智能重复”smartRepeat函数,

• 将3[abc]变为abcabcabc

• 将3[2[a]2[b]]变为aabbaabbaabb

• 将2[1[a]3[b]2[3[c]4[d]]]变为abbbcccddddcccddddabbbcccddddcccdddd

试实现:3[2[3[a]1[b]]4[d]]

  不用考虑输入字符串是非法的情况,比如:

• 2[a3[b]]是错误的,应该补一个1,即2[1[a]3[b]]

• [abc]是错误的,应该补一个1,即1[abc]

3、解题思路:

  遍历每一个字符时:

  • 如果这个字符是数字,那么就把数字压栈,把空字符串压栈
  • 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
  • 如果这个字符是],那么就将数字弹栈,把字符串栈的栈顶 的元素重复刚刚的这个次数,弹栈,拼接到新栈顶上
4、涉及到的正则表达式相关方法:

在这里插入图片描述
在这里插入图片描述

5、JS代码实现:
function smartRepeat(templateStr) {
    // 指针
    var index = 0;
    // 栈1,存放数字
    var stack1 = [];
    // 栈2,存放临时字符串
    var stack2 = [];
    // 剩余部分
    var rest = templateStr;

    while (index < templateStr.length - 1) {
        // 剩余部分
        rest = templateStr.substring(index);

        // 看当前剩余部分是不是以数字和[开头
        if (/^\d+\[/.test(rest)) {
            // 得到这个数字
            let times = Number(rest.match(/^(\d+)\[/)[1]);
            // 就把数字压栈,把空字符串压栈
            stack1.push(times);
            stack2.push('');
            // 让指针后移,times这个数字是多少位就后移多少位加1位。加的1位是[。
            index += times.toString().length + 1;
        } else if (/^\w+\]/.test(rest)) {
            // 如果这个字符是字母,那么此时就把栈顶这项改为这个字母
            let word = rest.match(/^(\w+)\]/)[1];
            stack2[stack2.length - 1] = word;
            // 让指针后移,word这个词语是多少位就后移多少位
            index += word.length;
        } else if (rest[0] == ']') {
            // 如果这个字符是],那么就①将stack1弹栈,②stack2弹栈,③把字符串栈的新栈顶的元素重复刚刚弹出的那个字符串指定次数拼接到新栈顶上。
            let times = stack1.pop();
            let word = stack2.pop();
            // repeat是ES6的方法,比如'a'.repeat(3)得到'aaa'
            stack2[stack2.length - 1] += word.repeat(times);
            index++;
        }

        console.log(index, stack1, stack2);
    }

    // while结束之后,stack1和stack2中肯定还剩余1项。返回栈2中剩下的这一项,重复栈1中剩下的这1项次数,组成的这个字符串。如果剩的个数不对,那就是用户的问题,方括号没有闭合。
    return stack2[0].repeat(stack1[0]);
}

var result = smartRepeat('3[2[3[a]1[b]]4[d]]');
console.log(result);
6、运行结果:

在这里插入图片描述

五、手写实现AST抽象语法树

1. 新建文件夹,创建package.json

npm init

2. 安装依赖

在这里插入图片描述

3. 新建webpack.config.js文件,参考webpack官网进行配置
const path = require('path');

module.exports = {
    // 入口
    entry: './src/index.js',
    // 出口
    output: {
        // 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
        publicPath: 'xuni',
        // 打包出来的文件名,不会真正的物理生成
        filename: 'bundle.js'
    },
    devServer: {
        // 端口号
        port: 8080,
        // 静态资源文件夹
        contentBase: 'www'
    }
};
4. 新建 www 文件夹,在该文件夹中创建 index.html 文件,并引入 bundle.js 文件:

在这里插入图片描述

5. 将 package.json 文件中的 scripts 改成:
"dev": "webpack-dev-server"

在这里插入图片描述

6. 新建 src 文件夹,分别在该文件夹下创建 index.js 文件、parse.js 文件、parseAttrsString.js 文件:

其中 index.js 文件代码如下:

import parse from './parse.js';

var templateString = `<div>
    <h3 class="aa bb cc" data-n="7" id="mybox">你好</h3>
    <ul>
        <li>A</li>
        <li>B</li>
        <li>C</li>
    </ul>
</div>`;

const ast = parse(templateString);
console.log(ast);

parse.js 文件代码如下:

import parseAttrsString from './parseAttrsString.js';

// parse函数,主函数
export default function (templateString) {
    // 指针
    var index = 0;
    // 剩余部分
    var rest = '';
    // 开始标记
    var startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;
    // 结束标记
    var endRegExp = /^\<\/([a-z]+[1-6]?)\>/;
    // 抓取结束标记前的文字
    var wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;
    // 准备两个栈
    var stack1 = [];
    var stack2 = [{ 'children': [] }];

    while (index < templateString.length - 1) {
        rest = templateString.substring(index);
        // console.log(templateString[index]);
        if (startRegExp.test(rest)) {
            // 识别遍历到的这个字符,是不是一个开始标签
            let tag = rest.match(startRegExp)[1];
            let attrsString = rest.match(startRegExp)[2];
            // console.log('检测到开始标记', tag);
            // 将开始标记推入栈1中
            stack1.push(tag);
            // 将空数组推入栈2中
            stack2.push({ 'tag': tag, 'children': [], 'attrs': parseAttrsString(attrsString) });
            // 得到attrs字符串的长度
            const attrsStringLength = attrsString != null ? attrsString.length : 0;
            // 指针移动标签的长度加2再加attrString的长度,为什么要加2呢?因为<>也占两位
            index += tag.length + 2 + attrsStringLength;
        } else if (endRegExp.test(rest)) {
            // 识别遍历到的这个字符,是不是一个结束标签
            let tag = rest.match(endRegExp)[1];
            // console.log('检测到结束标记', tag);
            let pop_tag = stack1.pop();
            // 此时,tag一定是和栈1顶部的是相同的
            if (tag == pop_tag) {
                let pop_arr = stack2.pop();
                if (stack2.length > 0) {
                    stack2[stack2.length - 1].children.push(pop_arr);
                }
            } else {
                throw new Error(pop_tag + '标签没有封闭!!');
            }
            // 指针移动标签的长度加3,为什么要加2呢?因为</>也占3位
            index += tag.length + 3;
        } else if (wordRegExp.test(rest)) {
            // 识别遍历到的这个字符,是不是文字,并别不能是全空
            let word = rest.match(wordRegExp)[1];
            // 看word是不是全是空
            if (!/^\s+$/.test(word)) {
                // 不是全是空 
                // console.log('检测到文字', word);
                // 改变此时stack2栈顶元素中
                stack2[stack2.length - 1].children.push({ 'text': word, 'type': 3 });
            }
            // 指针移动标签的长度加3,为什么要加2呢?因为</>也占3位
            index += word.length;
        } else {
            index++;
        }
    }

    // 此时stack2就是我们之前默认放置的一项了,此时要返回这一项的children即可
    return stack2[0].children[0];
};

parseAttrsString.js 文件代码如下:

// 把attrsString变为数组返回
export default function (attrsString) {
    if (attrsString == undefined) return [];
    console.log(attrsString);
    // 当前是否在引号内
    var isYinhao = false
    // 断点
    var point = 0;
    // 结果数组
    var result = [];

    // 遍历attrsString,而不是你想的用split()这种暴力方法
    for (let i = 0; i < attrsString.length; i++) {
        let char = attrsString[i];
        if (char == '"') {
            isYinhao = !isYinhao;
        } else if (char == ' ' && !isYinhao) {
            // 遇见了空格,并且不在引号中
            console.log(i);
            if (!/^\s*$/.test(attrsString.substring(point, i))) {
                result.push(attrsString.substring(point, i).trim());
                point = i;
            }
        }
    }
    // 循环结束之后,最后还剩一个属性k="v"
    result.push(attrsString.substring(point).trim());

    // 下面的代码功能是,将["k=v","k=v","k=v"]变为[{name:k, value:v}, {name:k, value:v}, {name:k,value:v}];
    result = result.map(item => {
        // 根据等号拆分
        const o = item.match(/^(.+)="(.+)"$/);
        return {
            name: o[1],
            value: o[2]
        };
    });

    return result;
}
7. 运行,并打开 http://localhost:8080/ 网址:

npm run dev

在这里插入图片描述
  到此,该部分内容就学完了,以上是根据 Vue源码探秘之AST抽象语法树 视频内容整理的,希望帮到自己的同时能帮到大家。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
抽象语法树AST)是源代码的抽象语法结构的树状表现形式,用于表示编程语言的语法结构。在编译过程中,AST被用来表示代码的语法结构,并且可以通过遍历和操作AST来进行代码转换和优化。函数指针是指向函数的指针变量,在C语言中特别常见。引用所提到的指针是指在生成AST时,通过指针来判断和截取函数的相关信息。具体来说,可以通过指针来判断函数的起始位置、结束位置以及函数名等属性信息。这些属性信息可以被保存在AST的节点中,方便后续对函数的分析和处理。所以,抽象语法树中的函数指针可以用于表示函数的相关信息,以便在编译过程中对函数进行分析和优化。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [vue源码解析---AST抽象语法树](https://blog.csdn.net/qq_63358859/article/details/126119350)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Vue源码抽象语法树](https://blog.csdn.net/qq_43522998/article/details/121015548)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值