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

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

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

而这些解析成果仅仅一个AST目标,并不能直接用于烘托,所以咱们需要将AST目标转化为render函数,而这个进程便是咱们这一章要讲的内容。

baseCompile

回到baseCompile函数,这个函数是在组件挂载的进程中调用的,组件挂载的进程能够参阅【源码&库】Vue3的组件是怎么挂载的?

在挂载的进程中,咱们会生成AST目标,AST目标怎么生成的能够参阅【源码&库】Vue3的模板转化为AST的进程

咱们能够从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函数;

历史章节