AST 编译原理[抽象语法树]
AST 的大致流程
export function buildCompiler(template) {
// template 这个就是一段源代码
// 1. 生成AST树
const ast = baseParse(template);
// 2. 转换AST
transform(ast);
// 3. 根据AST生成源代码
return generate(ast)
}
在VUE3 monorepo 继续编写模版编译
packages/compiler-dom
// yarn init -y
// package.json
// name: "@vue/compiler-dom"
"buildOptions": {
"name": "VueCompilerDom",
"formats": [
"global"
]
}
// packages/compiler-dom/index.ts
// yarn install 生成软连接 去根上
// 修改script/dev.js 改一下 target='compiler-dom'
packages/compiler-dom/index.ts
// vue3 h('div', {}, '') 并不好如果dom元素太多就太复杂了 模版编译完了就是这个内容。
// 使用jsx语法 -> 编译完了对应的也是 h这种的 这种的是没有优化的
// vue3 template->render是有优化的。 patchFlg blockTree h过来的是没有的。
// template->render
// html -> ast -> transform -> generator 最后生成render
// vue-next-template-explorer 模版演绎 -- 这个看到的是转化后的
// ast-explorer // 语法转化包各种各样 选择对用工具进行转换 这个是转化AST的裸树
// AST的结构
loc
start 这个节点从哪开始
end 这个节点到哪结束
baseParser 就是用来搞这个事情的 // 所以用来构造一个baseParser来处理一下这个事情。
baseParse 解析器,由于这个解析器内容需要接的很多,所以采用策略模式
// 总的解析器 解析器
export function baseParse(template){
// 状态机,标识节点的信息 行列偏移量等等。
// 每解析一段就移除一段。
// 相当于一个token一个token的过。筛选过了就扔掉已经扫描过的
// 例如<script>console.log('admin')</script>
// <script > console . log 以此类推。 一个一个的token的过。
// 记录上下文的信息 初始化一个上下文信息
const context = createParserContext(template); // loc 里面的内容
// 根据上下文做解析 [{loc:{start:{}, end: {}}}]
// 解析的内容是放到children下面
const parseChildren = createParseChildren(context);
}
createParserContext 创建解析的上下文记录一些信息 初始化上下文
// template 这个内容要不停的被截取。
function createParserContext(template){
return {
line: 1, // 这个代码位于第几行
column: 1, // 第几列
offset: 0, // 偏移量 什么时候会用到这个比如eslint 检查哪里写的不对用的就是start end 做一些标识会用到
source: template, // 这个source要被不停的截取。等待source空了解析就结束了
origin: template, // 原来的模版字符串
}
}
isEndSource 是不是遍历完了
function isEndSource(ctx){
// ctx.source == '' 就结束了
return !ctx.source;
}
解析元素的
function parseElement(ctx){
}
解析动态值的
function parseTpl(ctx){
}
根据上下文算出行列的信息
function getCursor(ctx) {
// 返回行列和偏移量
let {
line,
column,
offset,
} = ctx;
return {
line,
column,
offset
}
}
// 删除以后计算出一个新的结束位置
function advancePosition(ctx, s, end){
// 如何去跟新上下文信息
// 如何更新是第几行
// 如何更新列
// 如何更新偏移量
/*
* `he
* ll
* o
* `
* 占了三行
* 行是遇到换行就+1
*/
// 列怎么鼓捣?
// 换行的第一个字符
let linePos = -1
// 现在知道了end 就可以往前循环它的每一个字符
let linesCount = 0 ;
for(let i = 0; i < end; i++){
// s.charCodeAt(i) == 10 // 10是换行
if(s.charCodeAt(i) == 10) {
linesCount++;
// 换行后的对一个字符的位置
linePos = i;
}
}
// 计算偏移量
ctx.offset += end;
ctx.line += linesCount;
ctx.column = linePos == -1 ? ctx.column + end : end - linePos
}
获取节点的信息位置
function getEleSlectionInfo(ctx, start){ // 获取文本节点开始结束的位置
let end = getCursor(ctx); // 在getContentByContext已经变了所以拿到是获取后的最新位置
return {
start,
end,
source: ctx.origin.slice(start.offset, end.offset)
}
}
在上下文中把文本内容删除掉
function advanceBy(ctx, end){
let s = ctx.source; // 原内容
// 计算出一个新的结束位置 end的位置
// 更新一个新的 line col offset 的信息
advancePosition(ctx, s, end);// 根据 s 这个内容更新上下文
// 更新他的内容
ctx.source = ctx.source.slice(end);
}
根据游标获取内容
function getContentByContext(ctx, end){
// 获取到了文本内容
const rowTxt = ctx.source.slice(0, end);
// 在ctx的source 中把文本内容删除掉。
advanceBy(ctx, end)
return rowTxt;
}
解析文本的静态文本
// 处理文本该如何处理?
function parseTxt(ctx) {
// 获取基本信息 line column offset
// <div>test {{test}}</div>
// 文本是这里 test {{test}}
// 定位一下文本是从哪到哪?
// 词法分析
// 碰见的token
const endTokens = ['<', '{{']; // 遇到尖叫号和大括号就结束了
const endIndex = ctx.source.length; // 文本的长度
// 假设遇到<这个是结尾,在拿{{这个去比较谁在前就是到哪就是最近结尾
// <div>aaaa</div>{{test}}
// <比{{靠前
for(let i = 0; i < endTokens.length; i++){
// 从第一个开始找
const i = ctx.source.indexOf(endTokens[i], 1);
if(i != -1 && endIndex > i){
endIndex = i;
}
}
// endIndex 得到了一个最近的边界
// hello<div></div>
// 有了行列信息就开始进行更新
// 根据上下文 算出来个位置
// 可以拿到一个hello的开始, 字符串的开始没啥变化默认就是 字符串的 就是第一行第一列偏移量0
// 第一次这里的都是没有变化的默认的
// const context = createParserContext(template); // loc 里面的内容
let start = getCursor(ctx);
// 根据start 和 end 把字符串拿出来
// 根据游标获取内容
const resultStr = getContentByContext(ctx, endIndex);
// 当前文本的开始位置
// 当前文本的结束位置
return {
type: 2, // 这是个字典可以自己拿出去, 文本是2
resultStr, // 截取完毕以后的
loc: getEleSlectionInfo(ctx, start)
}
}
createParseChildren根据上下文解析孩子。
function createParseChildren(ctx){ // 根据内容做不同处里
const nodes = []; // vue3不需要跟节点所以这里什么类型的节点都是可以放的 没有标签默认就加Fragment
while(!isEndSource(ctx)){ // 没完成就继续
const s = ctx.source; // 分析当前上下文的内容
let node = null; // 通过策略创建的节点。
// 开始分析token
if(s[0] === '<') { // '<' 就是一个token
// 这是一个标签
node = parseElement(ctx);
} else if(s.startsWith("{{")) {
// 这是一个动态表达式
node = parseTpl(ctx)
} else {
// 文本静态类型的值
node = parseTxt(ctx);
}
nodes.push(node);
}
return nodes;
}