【源码&库】Vue3模版解析后的AST转换为render函数的过程

上一章我们详细的分析了Vue3的模版解析过程,每种不同的节点都对应着不同的解析结果;

而这些解析结果只是一个AST对象,并不能直接用于渲染,所以我们需要将AST对象转换为render函数,而这个过程就是我们这一章要讲的内容。

baseCompile

回到baseCompile函数,这个函数是在组件挂载的过程中调用的,组件挂载的过程可以参考【源码&库】Vue3的组件是如何挂载的?

在挂载的过程中,我们会生成AST对象,AST对象如何生成的可以参考【源码&库】Vue3的模板转换为AST的过程

我们可以从Vue3的模板转换为AST的过程这篇文章中看到baseCompile函数的调用过程,baseCompile函数如下:

js 复制代码
function baseCompile(template, options = {}) {
    const onError = options.onError || defaultOnError;
    const isModuleMode = options.mode === "module";
    {
        if (options.prefixIdentifiers === true) {
            onError(createCompilerError(47));
        } else if (isModuleMode) {
            onError(createCompilerError(48));
        }
    }
    const prefixIdentifiers = false;
    if (options.cacheHandlers) {
        onError(createCompilerError(49));
    }
    if (options.scopeId && !isModuleMode) {
        onError(createCompilerError(50));
    }
    
    // 这一步就是我们上两章讲的模版解析过程
    const ast = isString(template) ? baseParse(template, options) : template;
    
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset();
    
    // 下面的 transform 和 generate 函数就是我们这一章要讲的内容
    transform(
        ast,
        extend({}, options, {
            prefixIdentifiers,
            nodeTransforms: [
                ...nodeTransforms,
                ...options.nodeTransforms || []
                // user transforms
            ],
            directiveTransforms: extend(
                {},
                directiveTransforms,
                options.directiveTransforms || {}
                // user transforms
            )
        })
    );
    return generate(
        ast,
        extend({}, options, {
            prefixIdentifiers
        })
    );
}

由于之前并没有放出baseCompile函数的所有源码,这里放出来完整源码方便大家有一个完整的认识;

同时也是让大家知道我花了两章的内容去阅读的源码,其实只是baseCompile函数的一次函数调用;

从这里也知道这个函数有多核心,而且源码阅读的过程也是非常枯燥且痛苦的,但是阅读完成之后会发现自己的收获是非常大的;

不多说,dddd(懂的都懂),我们继续往下看;

这一章我们将模板内容做稍微复杂一点,这样就能看到更多的AST节点,模板内容如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id='app'>
    <h1>Title</h1>
    <my-component :message="message"></my-component>
    <div @click="handleClick">{{ message }}</div>
</div>

</body>
<script src="./vue.global.js"></script>
<script>
    const {createApp, h} = Vue;

    const app = createApp({
        data() {
            return {
                message: 'hello vue'
            }
        },
        methods: {
            handleClick() {
                this.message = 'hello vue3';
            }
        }
    });

    const MyComponent = {
        props: ['message'],
        render() {
            return h('div', this.message);
        }
    };

    app.component('MyComponent', MyComponent);

    debugger;
    app.mount('#app');

</script>
</html>

如果想要深入的话可以更复杂化这个模板,例如增加注释节点、v-if、v-for等指令、v-model、插槽等等;

transform

transform函数之前,还有一个getBaseTransformPreset函数,这个函数的作用是获取nodeTransformsdirectiveTransforms

这个函数主要是用于返回一个预设的转换函数,用于解析v-ifv-forv-on等指令,这里就不详细讲解了,大家可以自行阅读源码;

我们直接来看transform函数,这个函数的作用是将AST对象转换为render函数,函数源码如下:

js 复制代码
/**
 * @param root    ast对象
 * @param options 编译的配置项,包括nodeTransforms、directiveTransforms等
 */
function transform(root, options) {
    // 创建一个 AST 转换上下文对象,这个上下文对象包含了 AST 树转换过程中所需的信息和状态
    const context = createTransformContext(root, options);
    
    // 遍历 AST 树的节点并应用转换函数
    traverseNode(root, context);
    
    // 静态节点提升
    if (options.hoistStatic) {
        hoistStatic(root, context);
    }
    
    // 生成根节点的代码
    if (!options.ssr) {
        createRootCodegen(root, context);
    }
    
    // 收集 ast 上下文 helpers 函数的 key 值 
    root.helpers = /* @__PURE__ */ new Set([...context.helpers.keys()]);
    
    // 收集 ast 上下文的组件
    root.components = [...context.components];
    
    // 收集 ast 上下文的指令
    root.directives = [...context.directives];
    
    // 收集 ast 上下文的导入
    root.imports = context.imports;
    
    // 收集 ast 上下文的静态节点提升数组
    root.hoists = context.hoists;
    
    // 收集 ast 上下文的临时变量
    root.temps = context.temps;
    
    // 收集 ast 上下文的缓存节点数组
    root.cached = context.cached;
}

这里的createTransformContext函数主要是创建一个AST转换上下文对象,总的来说就是一个对象,这个对象上面有很多属性和方法;

由于太多就不贴源码了,感兴趣的同学可以自行阅读源码,这里不做过多的讲解;

接下来我们看traverseNode函数,这个函数的作用是遍历AST树的节点并应用转换函数,函数源码如下:

js 复制代码
/**
 * @param node    ast对象
 * @param context 转换上下文对象
 */
function traverseNode(node, context) {
    // 将转换上下文的当前节点设置为当前 ast 节点,用于表示当前正在转换的节点
    context.currentNode = node;
    
    // 从上下文对象中获取节点转换函数的数组 nodeTransforms
    const {nodeTransforms} = context;
    
    // 初始化一个数组 exitFns 用于存储节点转换时的退出函数
    const exitFns = [];
    
    // 遍历执行节点转换函数
    for (let i2 = 0; i2 < nodeTransforms.length; i2++) {
        // 执行节点转换函数,并将返回值赋值给 onExit
        // 这里执行的通常都是一些转换函数,比如处理 v-if、v-for、v-on 等指令的转换函数
        // 返回的 onExit 通常是一个函数,用于在节点转换结束后执行
        const onExit = nodeTransforms[i2](node, context);
        
        // 如果有 onExit 函数,则将 onExit 函数添加到 exitFns 数组中
        if (onExit) {
            if (isArray(onExit)) {
                exitFns.push(...onExit);
            } else {
                exitFns.push(onExit);
            }
        }
        
        // 如果当前节点已经被替换了,则直接退出遍历
        // 这里通常表示当前节点被移除了,比如 v-if 指令的条件不满足时,会将当前节点移除
        // 所以就没有必要再遍历当前节点的子节点了
        if (!context.currentNode) {
            return;
        } else {
            node = context.currentNode;
        }
    }
    
    // 根据当前节点的类型,执行不同的遍历函数
    switch (node.type) {
        // 3 是注释节点
        case 3:
            if (!context.ssr) {
                context.helper(CREATE_COMMENT);
            }
            break;
        
        // 5 是插值表达式节点
        case 5:
            if (!context.ssr) {
                context.helper(TO_DISPLAY_STRING);
            }
            break;
            
        // 9 是条件节点
        case 9:
            for (let i2 = 0; i2 < node.branches.length; i2++) {
                traverseNode(node.branches[i2], context);
            }
            break;
            
        
        case 10: // 10 是元素节点
        case 11: // 11 是组件节点
        case 1: // 1 是元素节点
        case 0: // 0 是根节点
            traverseChildren(node, context);
            break;
    }
    
    // 确保最终状态是当前节点
    context.currentNode = node;
    
    // 执行 exitFns 数组中的所有函数
    // 因为组件会有子节点,有些操作是在子节点转换完成后才能执行
    let i = exitFns.length;
    while (i--) {
        exitFns[i]();
    }
}

这里的traverseNode函数主要是遍历AST树的节点并应用转换函数,执行这一部分之后,AST的结构会发生很大的变化,可以简单看看对比:

generate

转换完成之后就该调用generate函数了,这个函数的作用是将AST对象转换为render函数源代码,动态生成render函数,函数源码如下:

js 复制代码
/**
 * @param ast     ast对象
 * @param options 编译的配置项,包括nodeTransforms、directiveTransforms等
 */
function generate(ast, options = {}) {
    // 创建了代码生成上下文,包含了生成代码所需的各种信息和工具函数
    // 和其他的上下文对象一样,这个上下文对象也是一个对象,上面有很多属性和方法,就不贴源码了
    const context = createCodegenContext(ast, options);
    
    // 是否通过指定上下文进行创建
    if (options.onContextCreated)
        options.onContextCreated(context);
    
    // 解构出上下文对象中的以下属性
    const {
        mode,
        push,
        prefixIdentifiers,
        indent,
        deindent,
        newline,
        scopeId,
        ssr
    } = context;
    
    // 根据上面的截图我们知道 helpers 里面存放的是一些 Symbol 类型的变量
    // 这个会在下面有使用
    const helpers = Array.from(ast.helpers);
    const hasHelpers = helpers.length > 0;
    
    // 根据选项和模式,确定是否使用 with 块。
    const useWithBlock = !prefixIdentifiers && mode !== "module";
    
    // 用于标识设置函数是否已经内联
    // 这里是因为构建之后产生的代码,所以固定为 false,源码会根据情况进行判断
    const isSetupInlined = false;
    const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context;
    
    // 生成函数的前导部分,包括可能的 with 块、帮助函数的引入等
    genFunctionPreamble(ast, preambleContext);
    
    // 根据是否为服务端渲染,确定函数的名称
    const functionName = ssr ? `ssrRender` : `render`;
    
    // 根据是否为服务端渲染,确定函数的参数列表
    const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];
    
    // 生成函数的开头部分,包括函数名、参数列表等
    // 会生成:function render(_ctx, _cache) {
    const signature = args.join(", ");
    push(`function ${functionName}(${signature}) {`);
    
    // 增加缩进
    indent();
    
    // 生成 with 块
    if (useWithBlock) {
        push(`with (_ctx) {`);
        indent();
        if (hasHelpers) {
            // 如果有 helpers,则生成 helpers 的引入
            // 根据上面的截图,我们可以知道生成的内容是:
            // const { createElementVNode, resolveComponent, ... } = _Vue
            push(`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue`);
            
            // 添加换行
            push(`
`);
            newline();
        }
    }
    
    // 生成组件代码
    if (ast.components.length) {
        genAssets(ast.components, "component", context);
        if (ast.directives.length || ast.temps > 0) {
            newline();
        }
    }
    
    // 生成指令代码
    if (ast.directives.length) {
        genAssets(ast.directives, "directive", context);
        if (ast.temps > 0) {
            newline();
        }
    }
    
    // 生成临时变量代码
    if (ast.temps > 0) {
        push(`let `);
        for (let i = 0; i < ast.temps; i++) {
            push(`${i > 0 ? `, ` : ``}_temp${i}`);
        }
    }
    
    // 如果有组件、指令、临时变量,则添加换行
    if (ast.components.length || ast.directives.length || ast.temps) {
        push(`
`);
        newline();
    }
    
    // 不是服务端渲染,生成 return 语句
    if (!ssr) {
        push(`return `);
    }
    
    // 生成子节点代码
    if (ast.codegenNode) {
        genNode(ast.codegenNode, context);
    } else {
        push(`null`);
    }
    
    // 如果使用 with 块,减少缩进并生成块的结束
    if (useWithBlock) {
        deindent();
        push(`}`);
    }
    
    // 减少缩进并生成函数的结束
    deindent();
    push(`}`);
    
    // 返回一个对象,包含生成的 AST、生成的代码、前导代码(如果设置函数已内联)、以及生成的 Source Map。
    return {
        ast,
        code: context.code,
        preamble: isSetupInlined ? preambleContext.code : ``,
        // SourceMapGenerator does have toJSON() method but it's not in the types
        map: context.map ? context.map.toJSON() : void 0
    };
}

这个函数本身其实已经很复杂了,这里光看代码上面的注释可能还是不太好理解,我就简单的给再梳理一下:

  1. 首先是创建了一个代码生成上下文,这个上下文对象中有一个code属性,这个属性就是最终生成的render函数源代码;
  2. 执行genFunctionPreamble函数生成函数的前导部分,包括全局变量的引入、静态节点的提升等;
  3. 生成函数的开头部分,包括函数名、参数列表、with块等;
  4. 生成组件、指令、临时变量代码;
  5. 生成return语句之后再生成子节点代码
  6. 生成子节点代码;
  7. 生成函数的结束;

这里注意的是第5步,因为render函数返回的是一个VNode节点,所以在生成return语句之后再生成子节点代码,并返回创建子节点的创建代码;

这里我们推荐深挖的就是genFunctionPreamblegenNode这两个函数,这两个函数的作用是生成函数的前导部分和生成子节点代码;

genFunctionPreamble

这个函数的作用是生成函数的前导部分,包括全局变量的引入、静态节点的提升等,函数源码如下:

js 复制代码
function genFunctionPreamble(ast, context) {
    // 解构出上下文对象中的以下属性
    const {
        ssr,
        prefixIdentifiers,
        push,
        newline,
        runtimeModuleName,
        runtimeGlobalName,
        ssrRuntimeModuleName
    } = context;
    
    // 获取运行在全局的 Vue 的名称,这里就是 Vue
    const VueBinding = runtimeGlobalName;
    
    // 辅助函数的名称
    const helpers = Array.from(ast.helpers);
    if (helpers.length > 0) {
        // 这里就相当于是一个别名,后续的代码全都会写死用这个别名获取全局的 Vue
        // const _Vue = Vue
        push(`const _Vue = ${VueBinding}
`);
        
        // 如果有静态节点提升,则生成静态节点提升的需要用到的辅助函数
        if (ast.hoists.length) {
            // 这里的静态节点提升的辅助函数会有如下一些
            const staticHelpers = [
                CREATE_VNODE, // 创建虚拟节点
                CREATE_ELEMENT_VNODE, // 创建元素虚拟节点
                CREATE_COMMENT, // 创建注释节点
                CREATE_TEXT, // 创建文本节点
                CREATE_STATIC // 创建静态节点
            ].filter((helper) => helpers.includes(helper)).map(aliasHelper).join(", ");
            
            // 通过别名获取辅助函数,并生成代码,这里的 staticHelpers 会生成一个解构赋值的键值对代码
            // const { createVNode: _createVNode, ... } = _Vue
            push(`const { ${staticHelpers} } = _Vue
`);
        }
    }
    
    // 生成静态节点提升的代码
    genHoists(ast.hoists, context);
    
    // 换行
    newline();
    
    // 生成 return 语句
    push(`return `);
}

这里的genFunctionPreamble函数内容并不复杂,主要是生成函数的前导部分,包括全局变量的引入、静态节点的提升等;

接下来看看genHoists都做了什么,函数源码如下:

js 复制代码
function genHoists(hoists, context) {
    // 如果没有静态节点提升,则直接返回
    if (!hoists.length) {
        return;
    }
    
    // 将代码生成上下文中的 pure 属性设置为 true,表示生成的代码是纯粹的表达式,没有副作用
    context.pure = true;
    const { push, newline, helper, scopeId, mode } = context;
    newline();
    
    // 生成静态节点提升的代码
    for (let i = 0; i < hoists.length; i++) {
        // 获取静态节点提升的节点
        const exp = hoists[i];
        if (exp) {
            // 使用索引命名保持变量名唯一性
            push(
                `const _hoisted_${i + 1} = ${``}`
            );
            
            // 使用 genNode 生成生成节点创建代码
            genNode(exp, context);
            newline();
        }
    }
    
    // 重置 pure 属性为 false,表示生成的代码可能有副作用
    context.pure = false;
}

可以看到这里核心也是使用genNode来生成节点创建代码,我们继续;

genNode

这个函数的作用是生成子节点代码,函数源码如下:

js 复制代码
function genNode(node, context) {
    // 如果是 string 类型,则直接作为代码
    if (isString(node)) {
        context.push(node);
        return;
    }
    
    // 如果是 symbol 类型,则使用辅助函数生成代码
    if (isSymbol(node)) {
        context.push(context.helper(node));
        return;
    }
    
    // 根据节点类型,执行不同的生成函数
    switch (node.type) {
        case 1: // 元素节点
        case 9: // if
        case 11: // for
            // 这些节点直接递归生成
            assert(
                node.codegenNode != null,
                `Codegen node is missing for element/if/for node. Apply appropriate transforms first.`
            );
            genNode(node.codegenNode, context);
            break;
        case 2: // 文本节点
            genText(node, context);
            break;
        case 4: // 表达式节点
            genExpression(node, context);
            break;
        case 5: // 插值节点
            genInterpolation(node, context);
            break;
        case 12: // fragment 节点
            genNode(node.codegenNode, context);
            break;
        case 8: // 复合表达式节点
            genCompoundExpression(node, context);
            break;
        case 3: // 注释节点
            genComment(node, context);
            break;
        case 13: // 生成 createVNode 的调用
            genVNodeCall(node, context);
            break;
        case 14: // 生成普通函数调用
            genCallExpression(node, context);
            break;
        case 15: // 生成对象表达式
            genObjectExpression(node, context);
            break;
        case 17: // 生成数组表达式
            genArrayExpression(node, context);
            break;
        case 18: // 生成函数表达式
            genFunctionExpression(node, context);
            break;
        case 19: // 生成条件表达式
            genConditionalExpression(node, context);
            break;
        case 20: // 生成缓存表达式
            genCacheExpression(node, context);
            break;
        case 21: // 生成节点列表
            genNodeList(node.body, context, true, false);
            break;
        case 22:
            break;
        case 23:
            break;
        case 24:
            break;
        case 25:
            break;
        case 26:
            break;
        case 10:
            break;
        default:
        {
            assert(false, `unhandled codegen node type: ${node.type}`);
            const exhaustiveCheck = node;
            return exhaustiveCheck;
        }
    }
}

这里的genNode函数主要是根据节点类型,执行不同的生成函数,可以看到具体的函数有很多,由于篇幅以及内容的复杂性,这里就留到下一章了;

最后我们可以看看生成的render函数源代码,如下:

js 复制代码
const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue

const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Title", -1 /* HOISTED */)
const _hoisted_2 = ["onClick"]

return function render(_ctx, _cache) {
    with (_ctx) {
        const { createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

        const _component_my_component = _resolveComponent("my-component")

        return (_openBlock(), _createElementBlock(_Fragment, null, [
            _hoisted_1,
            _createVNode(_component_my_component, { message: message }, null, 8 /* PROPS */, ["message"]),
            _createElementVNode("div", { onClick: handleClick }, _toDisplayString(message), 9 /* TEXT, PROPS */, _hoisted_2)
        ], 64 /* STABLE_FRAGMENT */))
    }
}

这里只需要将这段源码交给new Function执行,就可以生成一个函数代码,new Function的参数就是函数体的代码,所以上面的代码是直接写了return的,小知识点;

总结

这一章我们主要是讲解了AST对象转换为render函数的过程,这个过程主要是通过transformgenerate函数来实现的;

tansform函数的作用是对AST树进行转换,并提供对应的辅助函数,用于后续的代码生成;

generate函数的作用是将AST对象转换为render函数源代码,动态生成render函数;

最后将生成的代码交给new Function进行生成一个可执行的函数,这个函数就是render函数;

历史章节

相关推荐
optimistic_chen几秒前
【Vue入门】scoped与组件通信
linux·前端·javascript·vue.js·前端框架·组件通信
SuperEugene6 分钟前
前端空值处理规范:Vue 实战避坑,可选链、?? 兜底写法|项目规范篇
前端·javascript·vue.js
前端百草阁7 分钟前
Vue3 Diff 算法详解
前端·javascript·vue.js·算法·前端框架
im_AMBER8 分钟前
前后端对接: ESM配置与React Router
前端·javascript·学习·react.js·性能优化·前端框架·ecmascript
学且思10 分钟前
使用import.meta.url实现传递路径动态加载资源
前端·javascript·vue.js
problc12 分钟前
OpenClaw 的前端用的React还是Vue?
前端·vue.js·react.js
凰轮15 分钟前
vue实现大文件切片上传
vue.js
冰暮流星15 分钟前
javascript里面的return语句讲解
开发语言·前端·javascript
步步为营DotNet19 分钟前
使用.NET 11的Native AOT提升应用性能
java·前端·.net
左耳咚22 分钟前
Claude Code 记忆系统与 CLAUDE.md
前端·人工智能·claude