Vue3 源码内参<四>Compiler-core 之 Transform

59 阅读3分钟

Vue3 源码内参<四>Compiler-core 之 Transform

往期文章(都有相应的源码)

本节代码演示地址,配合源码食用更开胃噢!

  • 代码是参照 mini-vue 实现,有兴趣也可以看原作者的实现

看得见的思考

  • 将用户写的代码变成 ast 的好处是哪些呢?
  • 解析成 ast 后为什么要转换呢?

template 经过 compile 中的 baseCompile 中的 parse 转换后生成了 ast 抽象语法树,ast 是一个对象,可以用来描述模板字符串,使其结构化的表示,它可以帮助开发者在代码层面进行静态分析和转换。将模板转换为 ast,编译器可以进行更高级的优化,以生成更高效的渲染函数代码。

拿到 ast 后,vue 再调用 transform 对 ast 节点进行转换处理。当前的 ast 只是初步的对 template 结构的描述,还需再进一步对 ast 进行处理。比如解析 v-if、v-for 等各种指令或者对节点进行分析将满足条件的节点打上标记然后静态提升。

那 transform 函数是如何工作呢?请看下文分解~

开局一张图

transform.png

上图是做了特殊处理,增加了 text 类型的节点的内容。简单来说就是 transform 就是进一步的处理 ast,变成 JavaScript ast。(文本替换、节点替换、添加 shapeFlag 等操作)

mini-vue 最简实现 compiler-core 中的 transform 模块

transform 的工作原理简单来说就是利用各种插件来处理 ast 中的节点。

插件?

为什么要用插件?

  • 插件可以让代码实现解偶

vue3 源码中的 transform 插件列表

image.png

修改 text

目标:将 type 为 text 的节点,额外添加字符。此示例从单元测试入手,这样更方便我们调试代码。

// transform.spec.ts
import { NodeTypes } from "../src/ast";
import { baseParse } from "../src/parse";
import { transform } from "../src/transform";

describe("transform", () => {
    it("happy path", () => {
        // 1. parse 后拿到初步的 ast
        const ast = baseParse("<div>hi,{{message}}</div>");

        // 使用插件的方式 修改指定的内容
        const plugin = (node) => {
            if (node.type === NodeTypes.TEXT) {
                node.content = node.content + " vue3";
            }
        };

        // 2. 将 ast 和 插件传入 transform 中
        transform(ast, {
          nodeTransforms: [plugin],
        });

        //  3. 验证修改
        const nodeText = ast.children[0].children[0];
        expect(nodeText.content).toBe("hi, vue3");
    });
});

transform

import { NodeTypes } from "./ast"

// options 提供更动态的传参方式
export function transform(root, options = {}) {
    // 1. 创建上下文本
    const context = createTransformContext(root, options)
    // 2. 遍历节点
    traverseNode(root, context)
    // 3. 创建根 
    createRootCodegen(root)
}

function createTransformContext(root: any, options: any): any {
    const context = {
        root,
        nodeTransforms: options.nodeTransforms || [], // 插件列表
    }
    return context
}

function traverseNode(node: any, context) {
    const nodeTransforms = context.nodeTransforms
    const exitFns: any = []
    for (let i = 0; i < nodeTransforms.length; i++) {
        // 调用插件
        const transform = nodeTransforms[i];
        const onExit = transform(node, context)
        if (onExit) exitFns.push(onExit)
    }

    switch (node.type) {
        case NodeTypes.ROOT:
        case NodeTypes.ELEMENT:
            traverseChildren(node, context)
            break;
        default:
            break;
    }
    let i = exitFns.length
    while (i--) {
        exitFns[i]()
    }
}

function traverseChildren(node: any, context: any) {
    const children = node.children
    for (let i = 0; i < children.length; i++) {
        const node = children[i];
        traverseNode(node, context)
    }
}

function createRootCodegen(root: any) {
    const child = root.children[0]
    if (child.type === NodeTypes.ELEMENT) {
        root.codegenNode = child.codegenNode
    } else {
        root.codegenNode = root.children[0]
    }
}

这样单测就能顺利通过了。拿到 ast 后,transform + 对应插件 后 -> 创建上下文本 -> 遍历节点 -> 创建根。

END

本文只是简单介绍了 transform 的工作流程,具体的还需去看 vue3 源码,只是有了大体的印象之后,看源码不会迷糊。