Vue3源码——JS AST编译为render函数

208 阅读4分钟

前言

关于模板编译,从模板字符串template到最终呈现页面,Vue对于这部分的处理过程是:

  1. 将编写好的 template模板,转换为AST
  2. 将 AST 转换为JS AST
  3. JS AST转换为render函数
  4. 执行render函数,生成vnode树
  5. 经过patch处理,将dom结构呈现在页面上

其中,第一步和第二步我们已经介绍过了,需要的朋友可以再回顾一下:

这一节,我们就接着往下分析,看一看最终的render函数是如何生成的。

generate函数

function baseCompile(template, options = {}) {
    ...
    // 将template转换为AST
    // 获取用于操作转换ast的方法
    // 将AST转换为JS AST
    ...
    // 将JS AST生成render函数
    return generate(
      ast,
      extend({}, options, {
        prefixIdentifiers
      })
    );
  }

这一节,我们终于来到了baseCompile函数的最后一步,生成render函数。可以看到,生成render函数的关键就在于generate函数

function generate(ast, options = {}) {
    // 创建代码生成上下文
    const context = createCodegenContext(ast, options);
    // 从上下文context分解出需要用到的变量及方法
    const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context;
    // 从ast中取出需要从vue中导入的函数变量数组helpers
    const helpers = Array.from(ast.helpers);
    const hasHelpers = helpers.length > 0;
    const useWithBlock = !prefixIdentifiers && mode !== "module";
    const isSetupInlined = false;
    const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context;
    ...
    // 生成静态提升代码
    genFunctionPreamble(ast, preambleContext);
    // 根据是否是ssr模式,决定函数名及内部参数
    const functionName = ssr ? `ssrRender` : `render`;
    const args = ssr ? ["_ctx", "_push", "_parent", "_attrs"] : ["_ctx", "_cache"];
    const signature = args.join(", ");
    
    // 将上面维护好的变量值,使用字符串拼接的方式维护进上下文context的code属性中
    push(`function ${functionName}(${signature}) {`);
    
    indent();
    if (useWithBlock) {
      // 处理带 with 的情况,Web 端运行时编译
      push(`with (_ctx) {`);
      indent();
      if (hasHelpers) {
        push(`const { ${helpers.map(aliasHelper).join(", ")} } = _Vue`);
        push(`
`);
        newline();
      }
    }
    // 如果ast上有组件相关,生成自定义组件声明代码
    if (ast.components.length) {
      genAssets(ast.components, "component", context);
      if (ast.directives.length || ast.temps > 0) {
        newline();
      }
    }
    // 如果ast上有指令相关,生成自定义指令声明代码
    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();
    }
    if (!ssr) {
      push(`return `);
    }
    // 生成创建 VNode 树的render函数
    if (ast.codegenNode) {
      genNode(ast.codegenNode, context);
    } else {
      push(`null`);
    }
    if (useWithBlock) {
      deindent();
      push(`}`);
    }
    deindent();
    push(`}`);
    return {
      ast,
      code: context.code,
      preamble: isSetupInlined ? preambleContext.code : ``,
      map: context.map ? context.map.toJSON() : void 0
    };
  }

上面我们对generate函数做了分析,可以看出,其实经过了之前步骤的处理,ast中的信息已经非常的清晰,在这个函数中我们所要做的就是把render函数给拼出来:

  • 首先,创建代码生成上下文,里面包含了拼接过程中用到的工具函数
  • 然后,根据当前模式决定我们维护的render函数的函数名以及参数
  • 之后,根据 AST 中的内容,将对应的代码拼接进上下文contextcode属性 中。
  • 最后,将拼接好的 render函数,以及初始的 AST 包装成对象,并 return

接下来,我们深入看一下generate函数中用到的一些内部方法的具体实现以及他们的作用。

创建代码生成上下文函数createCodegenContext

function createCodegenContext(ast, {
    mode = "function",
    prefixIdentifiers = mode === "module",
    sourceMap = false,
    filename = `template.vue.html`,
    scopeId = null,
    optimizeImports = false,
    runtimeGlobalName = `Vue`,
    runtimeModuleName = `vue`,
    ssrRuntimeModuleName = "vue/server-renderer",
    ssr = false,
    isTS = false,
    inSSR = false
  }) {
    const context = {
      mode,
      prefixIdentifiers,
      sourceMap,
      filename,
      scopeId,
      optimizeImports,
      runtimeGlobalName,
      runtimeModuleName,
      ssrRuntimeModuleName,
      ssr,
      isTS,
      inSSR,
      source: ast.loc.source,
      code: ``,
      column: 1,
      line: 1,
      offset: 0,
      indentLevel: 0,
      pure: false,
      map: void 0,
      // 获取需要引入的函数方法名
      helper(key) {
        return `_${helperNameMap[key]}`;
      },
      // 向上下文context的code属性中拼接字符
      push(code, node) {
        context.code += code;
      },
      // 缩进换行相关方法
      indent() {
        newline(++context.indentLevel);
      },
      deindent(withoutNewLine = false) {
        if (withoutNewLine) {
          --context.indentLevel;
        } else {
          newline(--context.indentLevel);
        }
      },
      newline() {
        newline(context.indentLevel);
      }
    };
    function newline(n) {
      context.push("\n" + `  `.repeat(n));
    }
    return context;
  }

createCodegenContext函数 主要的工作就是创建一个代码生成的上下文context,在context维护了我们需要的一些基本信息以及处理AST的一些工具函数

  • push:主要用于拼接。
  • indent 和 deindent:用于处理缩进相关
  • newline:用于处理换行。

生成静态提升相关代码genFunctionPreamble

function genFunctionPreamble(ast, context) {
    const {
      ssr,
      prefixIdentifiers,
      push,
      newline,
      runtimeModuleName,
      runtimeGlobalName,
      ssrRuntimeModuleName
    } = context;
    const VueBinding = runtimeGlobalName;
    const helpers = Array.from(ast.helpers);
    if (helpers.length > 0) {
        push(`const _Vue = ${VueBinding}
        // 如果有静态提升的代码,那么从helpers数组中取出静态提升过程中用到的函数名
        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(", ");
          push(`const { ${staticHelpers} } = _Vue
`);
        }
      }
    }
    // 生成静态提升代码
    genHoists(ast.hoists, context);
    newline();
    push(`return `);
  }

genFunctionPreamble函数要做的事情就是生成render函数的前置代码,主要是:

  • 如果有需要静态提升的代码,那么从helpers数组中取出静态提升过程中用到的函数名
  • 维护可以静态提升的代码。

这里举个栗子:

<div>
    <span> {{x}} </span>
    <div>123</div>
</div>

上面的模板代码经过template => AST => JS AST,最终维护好的JS AST是这个样子的:

image.png

这里我们可以清晰的看到ast中维护的helpershoists两个属性的内容,那么,通过genFunctionPreamble函数我们实际上处理得到的内容是这一部分:

image.png

生成render函数主体部分

在生成render函数的主体部分前,还会依次去处理AST上的helpers,components,directives, temps。这里,主要就是判断它们的内容是否为空,不为空则做相应的处理。

最后则是通过genNode函数,来生成render函数的主体部分,我们来看一下genNode函数

function genNode(node, context) {
    if (isString(node)) {
      context.push(node);
      return;
    }
    if (isSymbol(node)) {
      context.push(context.helper(node));
      return;
    }
    switch (node.type) {
      case 1 /* ELEMENT */:
      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 /* TEXT */:
        genText(node, context);
        break;
      case 4 /* SIMPLE_EXPRESSION */:
        genExpression(node, context);
        break;
      case 5 /* INTERPOLATION */:
        genInterpolation(node, context);
        break;
      case 12 /* TEXT_CALL */:
        genNode(node.codegenNode, context);
        break;
      case 8 /* COMPOUND_EXPRESSION */:
        genCompoundExpression(node, context);
        break;
      case 3 /* COMMENT */:
        genComment(node, context);
        break;
      case 13 /* VNODE_CALL */:
        genVNodeCall(node, context);
        break;
        ....
    }
  }

genNode函数是通过遍历AST上的codegenNode属性,根据对应node节点的type值,去调用不同的处理方法。

还是用我们上面的模板为例:

<div>
    <span> {{x}} </span>
    <div>123</div>
</div>
image.png

astcodegenNode属性带genNode函数,我们首先遇到的node对应的type13,所以相应的就会去调用genVNodeCall函数

genVNodeCall函数中,我们又会对该node节点的子节点进行依次处理,最终生成我们需要的render函数

image.png

最后

至此,我们终于是了解了Vue从模板template到render函数的整个编译过程

整个的过程,虽然繁琐,但是思路还是比较清晰的,这里对整个的过程再做一个总结:

  • template编译为AST的逻辑是:通过指针的不断移动,维护剩余的模板字符串,再针对不同的情况具体讨论,调用对应封装好的处理函数,这样一点一点的蚕食模板字符串template,最终将得到的内容拼接为我们最终想要的AST
  • AST 转换为JS AST的逻辑是:遍历第一步中维护的AST节点,生成节点的codegenNode信息,同时做一些静态提升等操作,最终维护出一份信息更加明确的数据出来。
  • 生成render函数逻辑是:结合前面步骤维护好的JS AST,遍历AST上的node节点,然后使用封装好的工具函数,通过不断的判断,将render函数一点一点的拼接起来。