Vue 【进阶】- AST 抽象语法树
1. AST 简介
在开发Vue的时候编译器会将模板语法编译成正常的HTML语法,而直接编译的时候是非常困难的,因此此时会借助AST抽象语法树进行周转,进而变为正常的HTML语法,使编译工作变得更加简单。
抽象语法树的本质上是一个JS对象,Vue在审视所有HTML结构时是以字符串的新式进行的,最终将其解析为JS对象。AST抽象语法树服务于模板编译,将一种语法翻译为另一种语法。在Vue中将模板语法编译为HTML语法,自己作为中转站。
2. 抽象语法树和虚拟节点的关系
AST 只会在 编译阶段 出现,用来描述 template 模板
虚拟DOM 只在运行时出现,用来描述 dom, render 的时候产生新的 虚拟dom
首次渲染是很消耗性能的,所以,用运行时版本,使用webpack -> vue-loader 可以将我们的 vue 文件在打包编译时就转成了一个个 render 函数
为什么 AST 不可以直接当 virtual DOM 呢?为什么我们要写 render 函数,而不是直接写虚拟 DOM?
vue 的响应式机制是数据改变时重新执行 render 函数,生成新的虚拟 dom,然后 diff 比对、渲染。如果直接编译成虚拟 dom,数据改变时如何更新虚拟 dom?AST 重新生成虚拟 dom?做不到呀,因为代码没变。
其实 AST 和 virtual DOM 的样子都差不多
AST的样子
虚拟dom的样子
3. 手撸解析为 AST 的过程
[https://www.cnblogs.com/caijinghong/p/16918948.html]建议先看这篇栈相关处理得算法
index.js
import parse from './parse' var template = ` <div> <h3>你好</h3> <ul> <li>A</li> <li>B</li> <li>C</li> </ul> </div> ` console.log(parse(template))
parse.js
// parse, 主函数 export default function(template) { template = template.replace(/\s*/g,'') // console.log(template) // 指针 let index = 0, // 剩余 tail = template, // 栈 stack = [], // 匹配头标签<div> RegExp1 = /^\<(\w+)\>/, // 匹配尾部 content</div>和前面内容 RegExp2 = /(.*?)(\<\/\w+\>)/; while(index < template.length) { if(RegExp1.test(tail)) { stack.push({ tag: RegExp.$1, type: 1, children: [] }) index += RegExp.$1.length + 2 tail = template.substring(index) } else if(RegExp2.test(tail) && !RegExp1.test(tail)){ let endstack = stack[stack.length - 1] if(RegExp.$1) { endstack.children.push({ text: RegExp.$1, type: 3 }) } let child = stack.pop() // 最后的返回值 if(!stack.length) return child stack[stack.length - 1].children.push(child) index += (RegExp.$1.length + RegExp.$2.length) tail = template.substring(index) } } }
最后得 AST 树
加入属性版
var template = ` <div> <h3 class="test" name="666" style="color:red">你好</h3> <ul> <li>A</li> <li>B</li> <li>C</li> </ul> </div> `
parse.js
import handleAttr from './handleAttr' // parse, 主函数 export default function(template) { // 去掉首尾 template = template.replace(/(^\s*|\s*$)/g,'') // 指针 let index = 0, // 剩余 tail = template, // 栈 stack = [], // 匹配头标签<div>,还能匹配属性props RegExp1 = /^\<(\w+)(\s*.*?)\>/, // 匹配尾部 content</div>和前面内容 RegExp2 = /(.*?)(\<\/\w+\>)/; while(index < template.length) { if(RegExp1.test(tail)) { let obj = { tag: RegExp.$1, type: 1, children: [] } // 有 attrs if(RegExp.$2) { obj.attrs = [handleAttr(RegExp.$2)] } stack.push(obj) // 加上'<>'的长度 index += (RegExp.$1.length + 2 + RegExp.$2.length) // 并且清掉剩下的头部的空格 tail = template.substring(index).replace(/(^\s*)/g,'') // 加上空格的长度 index += RegExp.$1.length } else if(RegExp2.test(tail) && !RegExp1.test(tail)){ let endstack = stack[stack.length - 1] if(RegExp.$1) { endstack.children.push({ text: RegExp.$1, type: 3 }) } let child = stack.pop() // 最后的返回值 if(!stack.length) return child stack[stack.length - 1].children.push(child) index += (RegExp.$1.length + RegExp.$2.length) tail = template.substring(index).replace(/(^\s*)/g,'') index += RegExp.$1.length } } }
handleAttr.js 处理属性对象
export default function(attrs) { let obj = {} let attrsToArr = attrs.split(' ') attrsToArr .filter(item => item) .forEach(item => { let arrData =item.split('=') obj[arrData[0]] = arrData[1] }) return obj }
最后处理得如下
属性的写法改进
原来的写法 split 通过空格来处理得数组有个缺陷,
<h3 class="test name" name="666" style="color:red">你好</h3>
,这样处理得数组就不对,所以进行改进
方法一(指针法)
一个变量作为保存结果的作用,每次成功匹配变回覆盖
遍历到等号前的字符串去前空格后(/(^\s*)/g),便可变量作为对象的key,清空变量
匹配到'"'则开始保存值value在此遇到'"'结束
方法2️⃣(正则)
handleAttr.js
export default function(attrs) { let obj = {} let attrsToArr = attrs.match(/(\w+\=\"[\w|\s|\:]+\")+/g) console.log(attrs, attrsToArr) attrsToArr .forEach(item => { let arrData =item.split('=') obj[arrData[0]] = arrData[1] }) return obj }
大功告成!!!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)