likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 11

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

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

baseCompile

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

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节点,模板内容如下:

<!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函数,函数源码如下:

/**
 * @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树的节点并应用转换函数,函数源码如下:

/**
 * @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的结构会发生很大的变化,可以简单看看对比:

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

generate

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

/**
 * @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

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

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都做了什么,函数源码如下:

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

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

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函数源代码,如下:

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函数;

历史章节

转载自:https://juejin.cn/post/7304540675667984395
评论
请登录