vue3——模板template转AST到render函数的过程

前言

这篇文章我们来探究一下 Vue3 关于模板编译是如何一步一步的到AST然后到render函数的。简单的来说,分为这几步:

  1. 将编写好的 template模板字符串,转换为AST
  2. AST 转换为JS AST
  3. JS AST 转换为render函数

基于 vue3 的功能点,一点一点的拆分出来.代码命名会保持和源码中的一致,方便大家通过命名去源码中查找逻辑。github地址:compiler-core

template模板转AST

这里我们在看源码之前,先看一下将 ``template转换为AST,到底这个 AST` 是长什么样子的:

html 复制代码
<div id="demo">
  <span id="text">{{ state.a }}</span>
</div>

上面代码转换为 AST 之后为:

baseCompile

进入 baseCompile函数,首先会判断 template 是否为字符串模板,如果是字符串模板的话,则对字符串模板template进行解析

js 复制代码
function baseCompile(template, options = {}) {
    ...
    const isModuleMode = options.mode === "module";
    
    const prefixIdentifiers = false;
    ...
    // 将template转换为AST
    const ast = isString(template) ? baseParse(template, options) : template;
    
    // 获取用于操作转换ast的方法
    const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(prefixIdentifiers);
    
    // 将AST转换为JS AST
    transform(
      ast,
      extend({}, options, {
        prefixIdentifiers,
        nodeTransforms: [
          ...nodeTransforms,
          ...options.nodeTransforms || []
        ],
        directiveTransforms: extend(
          {},
          directiveTransforms,
          options.directiveTransforms || {}
        )
      })
    );
    
    // 将JS AST生成render函数
    return generate(
      ast,
      extend({}, options, {
        prefixIdentifiers
      })
    );
  }

baseCompile函数的整体流程也比较清晰,主要做的事情正是我们在文章开头提到的三步:template => AST => JS AST => render

baseParse

js 复制代码
function baseParse(content, options = {}) {
    // 创建字符串解析上下文
    const context = createParserContext(content, options);
    // 获取起始位置
    const start = getCursor(context);
    // 创建AST
    return createRoot(
      parseChildren(context, 0 /* DATA */, []),
      getSelection(context, start)
    );
}

function createParserContext(content, rawOptions) {
    const options = extend({}, defaultParserOptions);
    let key;
    for (key in rawOptions) {
      options[key] = rawOptions[key] === void 0 ? defaultParserOptions[key] : rawOptions[key];
    }
    return {
      options,
      column: 1,
      line: 1,
      offset: 0,
      // 存储我们的模板字符串template
      originalSource: content,
      source: content,
      inPre: false,
      inVPre: false,
      onWarn: options.onWarn
    };
  }
  
 function getCursor(context) {
  const { column, line, offset } = context
  return { column, line, offset }
}

baseParse函数中:

  • 首先,通过createParserContext函数初始化一个字符串解析上下文
  • 然后,通过getCursor函数获取起始位置

这两步说到底都还是一些初始化的工作,我们生成AST的核心逻辑还是在最后createRoot(parseChildren(context, 0 /* DATA */, []), getSelection(context, start));

createRoot

js 复制代码
function createRoot(children, loc = locStub) {
    return {
      type: 0 /* ROOT */,
      children,
      helpers: /* @__PURE__ */ new Set(),
      components: [],
      directives: [],
      hoists: [],
      imports: [],
      cached: 0,
      temps: 0,
      codegenNode: void 0,
      loc
    };
}

createRoot函数 的作用就是将接收到的参数childrenloc)和其他一些初始化参数进行整合,最后返回出去,即为我们想要得到的AST

parseChildren

parseChildren函数比较长,如果一行行看可能会头皮发麻(可以参考文章开头提供的地址,看简化complier-core代码),现在看看parseChildren到底都做了什么(简化的代码):

js 复制代码
const parserChildren = (context: { source: string }, ancestors) => {
  const nodes: any = []
  // 循环解析 字符串。
  while (!isEnd(context, ancestors)) {
    let node
    const source = context.source

    // 字符串是以 {{ 开头的才需要处理
    if (source.startsWith(interpolationOpenDelimiter)) {
      // 插值
      node = parseInterpolation(context)
    } else if (source.startsWith(ElementCloseDelimiter)) { // source[0] === '<'
      // element
      if (/[a-z]/i.test(source[1])) {
        node = parserElement(context, ancestors)
      }
    }

    // 如果前面的的两个判断都没有命中,表示是文本。
    if (!node) {
      node = parseText(context)
    }
    nodes.push(node)
  }

  return nodes
}

parseChildren的整体思路还是比较清晰的,主要逻辑就是通过遍历模板字符串 ,并对字符串进行分情况讨论

  • {{ 开头

  • < 开头

    • <! 开头
    • </ 开头
    • <+ 字母开头
    • *<? 开头
  • 文本节点

以 {{ 开头

源码中当我们遍历模板字符串遇到以 {{ 开头时,进入下面逻辑:

js 复制代码
// context.options.delimiters 为 [`{{`, `}}`]
if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
  // 如果s是以{{开头
  node = parseInterpolation(context, mode);
}

parseInterpolation函数主要是负责处理插值,比如{{ msg }}。将字符串解析上下文context 传入parseInterpolation函数 拿到node节点。

js 复制代码
function parseInterpolation(context, mode) {
    // 这里open为 {{     close为 }}
    const [open, close] = context.options.delimiters;
    // 确定 }} 位置
    const closeIndex = context.source.indexOf(close, open.length);
    // 如果 不存在 }} 说明这里的模板字符串写法不规范,直接报错
    if (closeIndex === -1) {
      emitError(context, 25 /* X_MISSING_INTERPOLATION_END */);
      return void 0;
    }
    // 获取起始位置
    const start = getCursor(context);
    // 将指针向后移动,同是会将context.source的内容也更新为 跳过了{{ 之后的内容
    advanceBy(context, open.length);
    // 插值起点指针
    const innerStart = getCursor(context);
    // 插值终点指针
    const innerEnd = getCursor(context);
    // 插值内容长度
    const rawContentLength = closeIndex - open.length;
    // 获取插值内容,也就是{{和}}之间的内容
    const rawContent = context.source.slice(0, rawContentLength);
    // 内部会调用advanceBy方法,将指针向后移动,并获取插值内容
    const preTrimContent = parseTextData(context, rawContentLength, mode);
    // 插值内容去空格
    const content = preTrimContent.trim();
    // 如果存在空格,计算偏移值
    const startOffset = preTrimContent.indexOf(content);
    if (startOffset > 0) {
      advancePositionWithMutation(innerStart, rawContent, startOffset);
    }
    // 如果尾部存在空格,计算偏移值
    const endOffset = rawContentLength - (preTrimContent.length - content.length - startOffset);
    advancePositionWithMutation(innerEnd, rawContent, endOffset);
    // 指针向后移动至}}之后
    advanceBy(context, close.length);
    return {
      type: 5 /* INTERPOLATION */,
      content: {
        type: 4 /* SIMPLE_EXPRESSION */,
        isStatic: false,
        constType: 0 /* NOT_CONSTANT */,
        // 插值的内容
        content,
        loc: getSelection(context, innerStart, innerEnd)
      },
      loc: getSelection(context, start)
    };
  }

parseInterpolation函数在做的工作就是通过不断地移动指针,然后判断对应位置的内容,去维护相应的信息,每一步的作用在上面注释中已写的比较详细了。

其中,移动指针的方法是advanceBy

在函数内部,会从context中取出剩余的模板字符串source,根据要跳过的字符长度维护上下文context中的位置变量,最后将上下文context中的source赋值为指针移动后的剩余模板字符串

以 < 开头

当剩余的字符串以<开头时,如果不符合规范,则直接报错;否则,则会继续进行分情况讨论,这里不一一讲了。

普通文本节点

我们可以发现,解析文本节点用到的函数是 parseText

js 复制代码
function parseText(context, mode) {
    // 根据mode确定文本解析的结束标志
    const endTokens = mode === 3 /* CDATA */ ? ["]]>"] : ["<", context.options.delimiters[0]];
    let endIndex = context.source.length;
    // 确定文本解析的结束位置
    for (let i = 0; i < endTokens.length; i++) {
      const index = context.source.indexOf(endTokens[i], 1);
      if (index !== -1 && endIndex > index) {
        endIndex = index;
      }
    }
    // 获取指针位置
    const start = getCursor(context);
    // 解析文本,并将指针后移
    const content = parseTextData(context, endIndex, mode);
    return {
      type: 2 /* TEXT */,
      content,
      loc: getSelection(context, start)
    };
  }

function parseTextData(context, length, mode) {
    // 获取文本内容
    const rawText = context.source.slice(0, length);
    // 移动指针
    advanceBy(context, length);
    if (mode === 2 /* RAWTEXT */ || mode === 3 /* CDATA */ || !rawText.includes("&")) {
      return rawText;
    } else {
      return context.options.decodeEntities(
        rawText,
        mode === 4 /* ATTRIBUTE_VALUE */
      );
    }
  }

parseText函数的逻辑:

  • 首先,根据mode确定了结束的标志,一般来说都是[">", "{{"]
  • 然后,在剩余的模板字符串中去搜索结束标志,找到最近的结束位置 。而从开头到结束位置,这中间的字符串就是我们需要的文本内容
  • 移动指针,返回解析后的文本AST

AST转JS AST

transform函数

js 复制代码
function transform(root, options) {
    // 生成转换上下文
    const context = createTransformContext(root, options);
    // 遍历AST节点进行转换
    traverseNode(root, context);
    // 静态提升
    if (options.hoistStatic) {
      hoistStatic(root, context);
    }
    // 创建根节点
    if (!options.ssr) {
      createRootCodegen(root, context);
    }
    root.helpers = /* @__PURE__ */ new Set([...context.helpers.keys()]);
    root.components = [...context.components];
    root.directives = [...context.directives];
    root.imports = context.imports;
    root.hoists = context.hoists;
    root.temps = context.temps;
    root.cached = context.cached;
}

创建转换上下文

首先通过createTransformContext函数 创建了一个转换上下文context

js 复制代码
function createTransformContext(root, ...options) {
    const context = {
      // options
      prefixIdentifiers,
      hoistStatic,
      cacheHandlers,
      ...
      
      // state
      root,
      helpers: /* @__PURE__ */ new Map(),
      components: /* @__PURE__ */ new Set(),
      directives: /* @__PURE__ */ new Set(),
      ...
      
      // methods
      helper(name) {},
      removeHelper() {},
      helperString() {},
      ...
    };

    return context;
  }

可以看到,创建的转换上下文主要分为三块:optionsstatemethods

  • options 主要表示转换过程中的一些配置项。
  • state 主要用来维护转换过程中用到的一些状态数据。
  • methods 主要是一些工具函数,我们上面提到的转换元素和指令的方法 nodeTransformsdirectiveTransforms中就会用到这些工具函数。

遍历AST节点进行转换

上下文创建完毕,接下来,通过执行traverseNode函数,对AST节点进行转换:

js 复制代码
function traverseNode(node, context) {
    // 保存ast
    context.currentNode = node;
    // 从上下文中取出转换方法
    const { nodeTransforms } = context;
    // 退出函数数组
    const exitFns = [];
    // 遍历转换方法数组,并维护退出函数数组
    for (let i = 0; i < nodeTransforms.length; i++) {
      const onExit = nodeTransforms[i](node, context);
      if (onExit) {
        if (isArray(onExit)) {
          exitFns.push(...onExit);
        } else {
          exitFns.push(onExit);
        }
      }
      if (!context.currentNode) {
      // 节点被移除
        return;
      } else {
        node = context.currentNode;
      }
    }
    switch (node.type) {
      case 3 /* COMMENT */:
        if (!context.ssr) {
          // context的helper中添加CREATE_COMMENT辅助函数
          context.helper(CREATE_COMMENT);
        }
        break;
      case 5 /* INTERPOLATION */:
        if (!context.ssr) {
          // context的helper中添加TO_DISPLAY_STRING辅助函数
          context.helper(TO_DISPLAY_STRING);
        }
        break;
      case 9 /* IF */:
       // 递归遍历ast的分支节点
        for (let i = 0; i < node.branches.length; i++) {
          traverseNode(node.branches[i], context);
        }
        break;
      case 10 /* IF_BRANCH */:
      case 11 /* FOR */:
      case 1 /* ELEMENT */:
      case 0 /* ROOT */:
       // 遍历子节点
        traverseChildren(node, context);
        break;
    }
    context.currentNode = node;
    let i = exitFns.length;
    // 执行退出函数
    while (i--) {
      exitFns[i]();
    }
  }

traverseNode函数的逻辑:

  • 首先,从上下文context 中取出转换函数组成的数组nodeTransforms ,遍历nodeTransforms数组 ,并维护一个由退出函数组成的数组exitFns

  • 然后,根据ast的类型不同执行不同的逻辑:

    • COMMENTINTERPOLATION类型时,维护对应的辅助函数。
    • IF 类型时,递归调用traverseNode函数,遍历ast的分支节点。
    • IF_BRANCHFORELEMENTROOT类型时,调用traverseChildren函数遍历子节点。
  • 最后,统一执行第一步中维护的退出函数

这里,我们着重关注一下转换函数组成的数组nodeTransforms 。根据前面的逻辑,我们可以分析出nodeTransforms数组的内容为:

js 复制代码
[ 
transformOnce, 
transformIf,
transformMemo, 
transformFor, 
transformExpression, 
transformSlotOutlet, 
transformElement, 
trackSlotScopes, 
transformText 
]

根据每个转换函数的名字,我们也能大概猜测出对应函数的处理对象是什么。

而且,还有一个很关键的地方在于,我们在对ast 进行转换的时候,是依次遍历nodeTransforms数组 的,这些转换函数的执行是有很明确的先后顺序的。

从这一层面,我们也就能更进一步明确vue中的一个经典题目:v-if和v-for谁的优先级高?

关于转换函数,每个函数展开之后,内容都十分庞杂,深入去看内部逻辑会非常的头秃。这里我想用一个简单的说法来带大家认识这一部分到底都是做什么的:

  • 首先,我们可以把 ast 想象成一个产品的初始状态,把转换函数数组nodeTransforms 想象成一条流水线 ,而每个转换函数 就是流水线上负责不同模块的工人 或者车间
  • 我们将 ast 放上流水线,然后每个转换函数 也就是工人ast 走到自己面前的时候都会对ast 进行加工,向ast上去添加或者修改不同的信息。
  • 最开始的 ast 不过是将整个模板字符串 进行切割 然后堆在一起,而经过转换操作之后,将这些繁杂切片进行了整合 ,汇聚成一个拥有更多更直观信息的ast。

静态提升

静态提升的函数为hoistStatic

js 复制代码
function hoistStatic(root, context) {
    walk(
      root,
      context,
      // 根节点不可被静态提升
      isSingleElementRoot(root, root.children[0])
    );
  }

我们可以看到hoistStatic函数 就是执行以下walk函数

walk函数代码很杂,但在做的工作还是比较清晰,我们简单总结一下:

  • 如果节点是可以静态提升 的节点,则维护对应codegenNodepatchFlag属性,并将其维护进上下文contexthoist容器中。
  • 如果节点不是可静态提升 的节点,则判断是否有可静态提升的属性 ,如果有,则将其维护进上下文contexthoist容器中。
  • 分情况,对子节点进行递归调用walk方法

我们在写模板template 代码的时候,有一些节点是不涉及动态变量 的。对于一些并不会改变的节点 ,我们更新dom 的时候,还有import引入的内容,可以避开他们,这样也是对性能的一大优化。

这里关于静态提升举个例子吧,比如我有这样的一段模板代码:

html 复制代码
<div>
  <span>{{x}}</span>
  <div>123</div>
</div>

我们可以发现,子节点中<div>123</div>不管动态属性 x 怎么改变,它始终就是一个简单的div,内容为123,所以它就是一个静态节点 ,是可以被静态提升的。

最终我们生成的JS AST 中维护的hoists长这个样子:

最终生成的render函数 ,则会将静态节点提取出来:

这样就最大程度的做到了复用 ,在动态内容发生更新重新调用render的时候,也无需再重新执行创建静态节点相关的函数了。

创建根代码节点

AST转换JS AST还差最后一个步骤------创建根代码节点。

js 复制代码
function createRootCodegen(root, context) {
    const { helper } = context;
    const { children } = root;
    // 如果子节点是单一节点
    if (children.length === 1) {
      const child = children[0];
      if (isSingleElementRoot(root, child) && child.codegenNode) {
        const codegenNode = child.codegenNode;
        if (codegenNode.type === 13 /* VNODE_CALL */) {
          convertToBlock(codegenNode, context);
        }
        root.codegenNode = codegenNode;
      } else {
        root.codegenNode = child;
      }
    } else if (children.length > 1) {
      // 如果子节点是多个节点,也就是没有根标签
      let patchFlag = 64 /* STABLE_FRAGMENT */;
      let patchFlagText = PatchFlagNames[64 /* STABLE_FRAGMENT */];
      if (children.filter((c) => c.type !== 3 /* COMMENT */).length === 1) {
        patchFlag |= 2048 /* DEV_ROOT_FRAGMENT */;
        patchFlagText += `, ${PatchFlagNames[2048 /* DEV_ROOT_FRAGMENT */]}`;
      }
      // 创建一个fragment
      root.codegenNode = createVNodeCall(
        context,
        helper(FRAGMENT),
        void 0,
        root.children,
        patchFlag + (true ? ` /* ${patchFlagText} */` : ``),
        void 0,
        void 0,
        true,
        void 0,
        false
        /* isComponent */
      );
    }
  }

最后的createRootCodegen函数 就比较简单,判断模板template 是否有根节点 ,如果没有根节点,则创建一个fragment节点,来接收前面所维护的信息。

JS AST编译为render函数

generate函数

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

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

js 复制代码
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

js 复制代码
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

js 复制代码
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数组中取出静态提升过程中用到的函数名
  • 维护可以静态提升的代码。

这里举个栗子:

html 复制代码
<div>
    <span> {{x}} </span>
    <div>123</div>
</div>

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

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

生成render函数主体部分

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

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

js 复制代码
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值,去调用不同的处理方法。

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

html 复制代码
<div>
    <span> {{x}} </span>
    <div>123</div>
</div>

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

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

最后

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

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

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

参考文章:

Vue3源码------AST编译为JS AST # Vue3源码------JS AST编译为render函数

相关推荐
LEE8 小时前
前端转型全栈 00:AI 时代该学哪些,不该学哪些
前端·后端
晚安日记wanna9 小时前
SSR 为什么不适合登录态从水合冲突到缓存串号
前端·react.js·面试
晚安日记wanna9 小时前
压测 TPS 卡在 800CPU 只跑四成六步定位法
运维·面试·测试
aixingpan9 小时前
aixingpan.cn API开发文档:api_docs_bichart_natalvssolararc2接口指南
前端·php
无限压榨切图仔9 小时前
一个优惠券需求改了三端,我才明白全栈不是多学一门语言
前端·后端
晚安日记wanna9 小时前
批量请求失败只弹一个 Toast面试官想听五层
前端·面试·架构
晚安日记wanna9 小时前
TS const 类型参数一道面试题的四层追问
前端·面试·typescript
晚安日记wanna9 小时前
大表 DDL 面试翻车现场Online DDL 为什么还会锁死业务
数据库·面试·架构
用户921080262869 小时前
前端 Vue 专栏 09:Vue Router 与前端路由
前端
晚安日记wanna9 小时前
MySQL 主从延迟别只答并行复制
数据库·面试·架构