VUE AST 编译原理

1,784 阅读5分钟

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;
}