Vue2.0 —— 运用算法实现 AST 抽象语法树

《工欲善其事,必先利其器》

既然点进来了,麻烦你看下去,希望你有不一样的收获。

一、什么是 AST ?

AST,全称 Abstract Syntax Trees,中文名称为 抽象语法树它是源代码语法结构的一种抽象表示,以树状的形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。诸如,编辑器(IDE)和 模板语法的模板编译,又或是 Babel 的源代码编译,都应用到抽象语法树的思想。

例如:我们熟知的 Vue 的 template 模板语法,当我们在里面书写 HTML 代码的时候,就会经历以下几个步骤:

  • 通过 Vue-Loader 将 template 里面的语法转化为字符串;
  • 利用算法,将 template 模板字符串解析为 AST 抽象语法树;
  • 通过 patchVnodediff 算法将 AST 转化为虚拟节点继而是实现最小量更新。

这里我们通过一张图就可以很清晰的了解整个 Vue2.0 的模板编译机制响应式原理


这里可能会有人要问:

  • 我都有 HTML 模板字符串了,不能直接 innerHTML 嘛?它不香嘛?


这就说明你并不了解 Vue 框架的特性。

Vue 它是一个渐进式的 SPA(单页面)框架,视图无感更新和数据实时响应以及页面性能是必须要重点优化的地方。尤其是视图更新,想要做到以最小的性能损耗达到实时更新的目的,就必须借助 虚拟节点Diff 的手段,而不是每次改变数据就操作节点。

因此,为了实现虚拟节点,我们就需要先实现 AST

二、如何实现 Vue2.0 的 AST 解析?

AST 的解析,如果不考虑 Vue-Loader 的转化步骤,归根结底是一个模板字符串转化为树状结构的算法问题。那么是算法问题,就跟我们昨天刚学习的《栈数据结构》一样,我们先要分析怎么计算:

  • 首先,它是一个字符串,但是结果的结构变了,那么势必要用到指针的思想;
  • 其次,字符串有配对,,有嵌套的结构,那么相比递归,我会更加倾向与栈;
  • 指针方面,我选择用 while 而不是 for,因为 while 可以更加灵活的操控指针;
  • 最后,还要分析标签中属性的字符和空格,难度上升,对正则表达式有要求。

确定好算法之后,我会再分析一下计算的思路:

  1. 定义指针;
  2. 遍历模板字符串;
  3. 正则遇见开始标签,标签进栈;
  4. 正则遇见结束标签,标签弹栈;
  5. 正则遇见标签之间的文字,文字进栈。

那么话不多说,我们开始~

三、实现 AST 解析的基础过程

以下代码均在 node 环境下编写,需要的小伙伴们自行配置环境,或拉取文末的 git。

假设现有模板字符串如下:

var templateString = `<div><h3>你好</h3><ul><li>1</li><li>2</li><li>3</li></ul></div>
`

解析模板字符串的代码,可能会有点长,但是需要你仔细阅读:

export default function parse(templateString) {// 指针let index = 0;// 剩余部分let rest = templateString;// 标签栈let tagSections = [];// 字符栈,先用 children 占位,防止弹栈let charSections = [{ children: [] }];// 开始标签正则const startRegExp = /^\<([a-z]+[1-6]?)(\s[^\<]+)?\>/;// 结束标签正则const endRegExp = /^\<\/([a-z]+[1-6]?)\>/;// 中间文字正则const wordRegExp = /^([^\<]+)\<\/[a-z]+[1-6]?\>/;while(index < templateString.length - 1) {// 截取剩余部分rest = templateString.substring(index);// 若当前字符检测到开始标签if (startRegExp.test(rest)) {// 获取开始标签let tag = rest.match(startRegExp)[1];// 标签栈入栈tagSections.push(tag);// 字符栈入栈,用 children 和 tag 占位charSections.push({ "tag": tag, "children": [] });// 指针步进为 标签长度加2(< >)index += tag.length + 2;} else if (wordRegExp.test(rest)) {// 若当前字符检测到文字let word = rest.match(wordRegExp)[1];// 判断获取到的文字是否全为空if (!/^\s+$/.test(word)) {// 如果不是,则入栈,工作栈为字符栈的栈顶那一项charSections[charSections.length - 1].children.push({ "text": word, "type": 3 }); }// 指针步进为 文字长度index += word.length;} else if (endRegExp.test(rest)) {// 若当前字符检测到结束标签let endTag = rest.match(endRegExp)[1];// 标签栈弹栈let pop_tag = tagSections.pop();// 判断开始标签与结束标签是否闭合if (pop_tag == endTag) {let pop_char = charSections.pop();// 判断字符栈是否还存在工作栈,如果是则并入到上一个工作栈if (charSections.length > 0) {charSections[charSections.length - 1].children.push(pop_char);}} else {throw new Error(pop_tag + "标签没有闭合!!!");}// 指针步进为 结束标签长度加3(</ >)index += endTag.length + 3;} else {// 默认情况下指针自增index++;}}// 由于事先布置好占位数组,故字符栈现在存有一项,即总的数据,因此返回该项 childrenreturn charSections[0].children[0];
}

以上,我们的基础版 AST 模板解析已经完成,让我们看一下效果:

看起来好像是那么一回事了,以上的做法呢,可以说是简陋的实现了一下基本状况下的模板字符串吧。因为实际上的模板字符串可能还有这种结构:

  • <div>123</div> 456 <div>789</div>
  • <input type="text" value="val" />

这时候可能有小伙伴会说了:

  • 你这也太敷衍了啊,都说不上是源码解析啊!那我加一个 classid 怎么办?就放弃了嘛!!!

的确,我们接下来要继续来完善标签内属性的计算情况,只是你应该会看到,在代码中,你的算法如果有半点差错, 那么导致的结果将是灾难级别的。。。Vue2.0 源码中是处理了很多种情况下的模板字符串的,而我们这里只实现了基本情况下的核心算法而已。。可见源码有多么厉害。。。(捂脸)

四、完善标签内属性的 AST 解析

假设现有模板字符串如下:

var templateString = `<div class="container"><h3 id="title" class="title pt5">你好</h3><ul id="list"><li class="item">1</li><li class="item">2</li><li class="item">3</li></ul></div>
`

话不多说,继续写情况分析:

首先,我们先要截取到标签内,属性的字符串。

// 修改第三步的代码
while(index < templateString.length - 1) {// 识别遍历到的这个字符,是不是一个开始标签if (startRegExp.test(rest)) {// ...// 获取属性字符串let attrsString = rest.match(startRegExp)[2];// 将开始标记推入栈中stack1.push(tag);// 将空数组推入栈2中stack2.push({ "tag": tag, children: [], "attrs": parseAttrsString(attrsString) });// 得到attrs的总长度const attrsStringLength = attrsString != null ? attrsString.length : 0;// 指针移动标签的长度加2,因为<>也占两位,再加上属性字符串的长度index += tag.length + 2 + attrsStringLength;}// ...
}

新建处理 属性字符串 的函数:

export default function parseAttrsString(attrsString) {// 如果字符串不存在,变为数组返回if (attrsString == undefined) return [];// 判断检测到的空格是否包含在引号内let isInside = false;// 指针let index = 0;// 结果数组let result = [];// 遍历属性字符串for(let i = 0; i < attrsString.length; i++) {let char = attrsString[i];// 如果当前字符是双引号if (cahr == '"') {// 设置变量,包含开始双引号和结束双引号isInside = !isInside;} else if (char == " " && !isInside) {// 若当前字符是空格,但不在双引号之内,则截取剩余字符串// 排除了属性之外的空格字符let rest = attrsString.substring(index, i);// 判断截取到的字符是否全为空if (!/^\s*$/.test(rest)) {// 如果不是,则并入结果数组result.push(rest.trim());}// 移动指针index = i;}}// 循环结束之后,由于指针移动的比i慢,最后还会剩下一个属性未被并入// 清除字符串前后空格之后,将其并入结果数组result.push(attrsString.substring(index).trim());// 现结果数组里的数据结构为 ["k=v", "k=v", "k=v"] 这种// 但我们需要把它变为 [{name: k, value: v},{name: k, value: v},{name: k, value: v}]// 因此我们在知道数据结构的情况下,可以采用递归,并且是映射递归result = result.map(item => {// 根据等号拆分字段const path = item.match(/^(.+)="(.+)"$/);return {name: path[1],value: path[2]}})// 最后,返回结果数组return result;
}

让我们,看一下效果呀:

怎么样,没有辜负你的一片期望吧?哈哈哈哈哈哈哈哈哈嗝~

四、总结

这篇文章,我们目的是要实现 Vue2.0 源码中的 —— 《模板字符串解析为 AST 抽象语法树》。即使实现的情况只能适应基础状况下的模板语法,但我觉得也够用了。(其实就是累了~)

其中,我们解释了,为什么有 AST

因为一切都是为了它的特性,突出性能,才会选择这种处理方案的。至于,为什么 AST 不直接 Diff ?好问题。

  • 因为有时候你只是改动了 data 的数据,并没有修改 template 模板语句,所以此时你不可能再算一遍 AST 然后去比较吧?就算你去比较也 Diff 不出来任何变化的。
  • 其次,虚拟节点搭配 Diff 所产生的性能损耗是最小的,这就是为什么每次改变 data 数据时重新执行 render 函数,生成新的虚拟节点,然后 Diff 比对、渲染。
  • 最后,虚拟节点就是一个数据结构,render(patchVnode)函数是一段可执行的代码,你能在函数内触发更多其他条件。例如 watch,例如 computed 等等。

最后,感谢你的阅读,希望我的文章对你有所帮助。完整代码我已上传至码云,有需要的小伙伴自行查阅 —— 《传送地址》。

参考文献:

《B站尚硅谷 AST 源码视频》

Vue2.0 —— 运用算法实现 AST 抽象语法树相关推荐

  1. 超级详细AST抽象语法树Javascript

    AST 抽象语法树 Program 程序(开始) interface Program <: Node {type: "Program";body: [ Statement ] ...

  2. 详解AST抽象语法树

    浅谈 AST 先来看一下把一个简单的函数转换成AST之后的样子. // 简单函数 function square(n) {return n * n; }// 转换后的AST {type: " ...

  3. python树代码_浅析AST抽象语法树及Python代码实现

    在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码.树上的每个 ...

  4. 抽象语法树 c语言,AST抽象语法树

    AST抽象语法树 why主流项目插件的用途: javascript转译.代码压缩.css预处理.eslint.prettier等都建立在AST的基础上. whataccording to the gr ...

  5. php ast 抽象语法树,AST抽象语法树的基本思想

    AST抽象语法树的基本思想 前言 AST概述 AST结构 AST解析 转换 生成 前言 在阅读java ORM框架spring data jpa的源码时,发现Hibernate(spring data ...

  6. 基于Python的JS逆向和AST抽象语法树

    一.关于js逆向的一些知识点总结 javascript-数据类型中,null,undefinded都表示没有东西 普通函数,匿名函数,构造函数,一般常见的是匿名函数 函数可以当成字符串看待,可以看成参 ...

  7. java AST 抽象语法树

    AST : Abstract Syntax Tree https://www.geeksforgeeks.org/abstract-syntax-tree-ast-in-java/ 抽象语法树是一种用 ...

  8. 简述 AST 抽象语法树

    简述 AST 抽象语法树 一.概念 二.特点 三.用途 四.案例 一.概念 AST 全称为 Abstract Syntax Tree,译为抽象语法树.在 JavaScript 中,任何一个对象(变量. ...

  9. java抽象语法树(ast),AST 抽象语法树

    AST 抽象语法树简介 AST(Abstract Syntax Tree)是源代码的抽象语法结构树状表现形式,Webpack.ESLint.JSX.TypeScript 的编译和模块化规则之间的转化都 ...

  10. php ast 抽象语法树,抽象语法树(AST)

    抽象语法树入门到放弃? 抽象语法树(Abstract syntax tree AST)在计算机科学中,抽象语法和抽象语法树其实是源代码的抽象语法结构的树状表现形式 为什么是抽象的? 前端工程化,离不了 ...

最新文章

  1. 设计模式C#实现(三)——外观模式
  2. 表框mysql_mysql 表的操作
  3. BOM--location对象、history对象
  4. 转HTML中的table转为excel
  5. mvc3中正确处理ajax访问需要登录的页面
  6. 大型网站的架构设计问题—-大型高并发高负载网站的系
  7. OpenShift 4 - 基于URL的应用路由
  8. matlab 的 legend 用法
  9. HP笔记本ProBook 445R G6刷BIOS历险记
  10. Python文本处理,依次读取文本每一行,查找每一行特定位置的元素,生成列表,去重
  11. spring security中A granted authority textual representation is required
  12. 虚拟化服务器端口用万兆,虚拟化升级,千兆变万兆!
  13. mybatis中显示更新数据成功 ,控制台显示成功,数据库数据却没有修改
  14. gitee 链接报错
  15. Java MD5加密工具
  16. MuseScore批量化插件使用
  17. 云栖重磅!阿里云启动视频云V5计划,全面赋能生态合作伙伴
  18. 局域网arp攻击_什么是arp攻击?arp攻击的解决方法有哪些?
  19. 【机器学习】采用关联规则算法分析蔬菜涨跌规律实验(Apriori算法)
  20. angular7+underscore的使用

热门文章

  1. python读parquet文件 pandas读parquet文件
  2. 啥是甘特图?用思维导图制作甘特图的方法
  3. 【干货】旺季营销婚纱摄影行业这么做广告,转化效果能提高50%!
  4. AI如何识别西瓜和冬瓜?
  5. java 图片文字 乱码_关于java 向图片添加文字时的乱码问题
  6. 6v网站迅雷下载电影任务出错
  7. Hyper-v安装及使用详细教程
  8. 揭开HPC应用的神秘面纱
  9. 纪念我的小宝贝——浪浪
  10. matlab中建立变换器模型,基于Simulink/Matlab的DC-DC变换器系统仿真