<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    源碼淺析:vue3的 generate 是生成 render 函數(shù)的過程

    共 16989字,需瀏覽 34分鐘

     ·

    2024-05-23 08:02

    大家好,我是歐陽!

    本文約2220+字,整篇閱讀大約需要12分鐘。

    前言

    在之前的 面試官:來說說vue3是怎么處理內(nèi)置的v-for、v-model等指令? 文章中講了transform階段處理完v-for、v-model等指令后,會(huì)生成一棵javascript AST抽象語法樹。這篇文章我們來接著講generate階段是如何根據(jù)這棵javascript AST抽象語法樹生成render函數(shù)字符串的,本文中使用的vue版本為3.4.19。

    看個(gè)demo

    還是一樣的套路,我們通過debug一個(gè)demo來搞清楚render函數(shù)字符串是如何生成的。demo代碼如下:

    <template>
      <p>{{ msg }}</p>
    </template>

    <script setup lang="ts">
    import { ref } from "vue";

    const msg = ref("hello world");
    </script>

    上面這個(gè)demo很簡單,使用p標(biāo)簽渲染一個(gè)msg響應(yīng)式變量,變量的值為"hello world"。我們在瀏覽器中來看看這個(gè)demo生成的render函數(shù)是什么樣的,代碼如下:

    import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
    function _sfc_render(_ctx, _cache, $props$setup$data$options) {
      return _openBlock(), _createElementBlock(
        "p",
        null,
        _toDisplayString($setup.msg),
        1
        /* TEXT */
      );
    }

    上面的render函數(shù)中使用了兩個(gè)函數(shù):openBlockcreateElementBlock。在之前的 vue3早已具備拋棄虛擬DOM的能力了文章中我們已經(jīng)講過了這兩個(gè)函數(shù):

    • openBlock的作用為初始化一個(gè)全局變量currentBlock數(shù)組,用于收集dom樹中的所有動(dòng)態(tài)節(jié)點(diǎn)。

    • createElementBlock的作用為生成根節(jié)點(diǎn)p標(biāo)簽的虛擬DOM,然后將收集到的動(dòng)態(tài)節(jié)點(diǎn)數(shù)組currentBlock塞到根節(jié)點(diǎn)p標(biāo)簽的dynamicChildren屬性上。

    render函數(shù)的生成其實(shí)很簡單,經(jīng)過transform階段處理后會(huì)生成一棵javascript AST抽象語法樹,這棵樹的結(jié)構(gòu)和要生成的render函數(shù)結(jié)構(gòu)是一模一樣的。所以在generate函數(shù)中只需要遞歸遍歷這棵樹,進(jìn)行字符串拼接就可以生成render函數(shù)啦!

    generate函數(shù)

    首先給generate函數(shù)打個(gè)斷點(diǎn),generate函數(shù)在node_modules/@vue/compiler-core/dist/compiler-core.cjs.js文件中。

    然后啟動(dòng)一個(gè)debug終端,在終端中執(zhí)行yarn dev(這里是以vite舉例)。在瀏覽器中訪問  http://localhost:5173/ ,此時(shí)斷點(diǎn)就會(huì)走到generate函數(shù)中了。在我們這個(gè)場景中簡化后的generate函數(shù)是下面這樣的:

    function generate(ast) {
      const context = createCodegenContext();
      const { push, indent, deindent } = context;

      const preambleContext = context;
      genModulePreamble(ast, preambleContext);

      const functionName = `render`;
      const args = ["_ctx""_cache"];
      args.push("$props""$setup""$data""$options");
      const signature = args.join(", ");
      push(`function ${functionName}(${signature}) {`);

      indent();
      push(`return `);
      genNode(ast.codegenNode, context);

      deindent();
      push(`}`);
      return {
        ast,
        code: context.code,
      };
    }

    generate中主要分為四部分:

    • 生成context上下文對象。

    • 執(zhí)行genModulePreamble函數(shù)生成:import { xxx } from "vue";

    • 生成render函數(shù)中的函數(shù)名稱和參數(shù),也就是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {

    • 生成render函數(shù)中return的內(nèi)容

    context上下文對象

    context上下文對象是執(zhí)行createCodegenContext函數(shù)生成的,將斷點(diǎn)走進(jìn)createCodegenContext函數(shù)。簡化后的代碼如下:

    function createCodegenContext() {
      const context = {
        code: ``,
        indentLevel: 0,
        helper(key) {
          return `_${helperNameMap[key]}`;
        },
        push(code) {
          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;
    }

    為了代碼具有較強(qiáng)的可讀性,我們一般都會(huì)使用換行和鎖進(jìn)。context上下文中的這些屬性和方法作用就是為了生成具有較強(qiáng)可讀性的render函數(shù)。

    • code屬性:當(dāng)前生成的render函數(shù)字符串。

    • indentLevel屬性:當(dāng)前的鎖進(jìn)級別,每個(gè)級別對應(yīng)兩個(gè)空格的鎖進(jìn)。

    • helper方法:返回render函數(shù)中使用到的vue包中export導(dǎo)出的函數(shù)名稱,比如返回openBlock、createElementBlock等函數(shù)

    • push方法:向當(dāng)前的render函數(shù)字符串后插入字符串code。

    • indent方法:插入換行符,并且增加一個(gè)鎖進(jìn)。

    • deindent方法:減少一個(gè)鎖進(jìn),或者插入一個(gè)換行符并且減少一個(gè)鎖進(jìn)。

    • newline方法:插入換行符。

    生成import {xxx} from "vue"

    我們接著來看generate函數(shù)中的第二部分,生成import {xxx} from "vue"。將斷點(diǎn)走進(jìn)genModulePreamble函數(shù),在我們這個(gè)場景中簡化后的genModulePreamble函數(shù)代碼如下:

    function genModulePreamble(ast, context) {
      const { push, newline, runtimeModuleName } = context;
      if (ast.helpers.size) {
        const helpers = Array.from(ast.helpers);
        push(
          `import { ${helpers
            .map((s) => `${helperNameMap[s]} as _${helperNameMap[s]}`)
            .join(", ")} } from ${JSON.stringify(runtimeModuleName)}
    `,
          -1 /* End */
        );
      }
      genHoists(ast.hoists, context);
      newline();
      push(`export `);
    }

    其中的ast.helpers是在transform階段收集的需要從vue中import導(dǎo)入的函數(shù),無需將vue中所有的函數(shù)都import導(dǎo)入。在debug終端看看helpers數(shù)組中的值如下圖:

    從上圖中可以看到需要從vue中import導(dǎo)入toDisplayStringopenBlock、createElementBlock這三個(gè)函數(shù)。

    在執(zhí)行push方法之前我們先來看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到此時(shí)生成的render函數(shù)字符串還是一個(gè)空字符串,執(zhí)行完push方法后,我們來看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到此時(shí)的render函數(shù)中已經(jīng)有了import {xxx} from "vue"了。

    這里執(zhí)行的genHoists函數(shù)就是前面 搞懂 Vue 3 編譯優(yōu)化:靜態(tài)提升的秘密文章中講過的靜態(tài)提升的入口。

    生成render函數(shù)中的函數(shù)名稱和參數(shù)

    執(zhí)行完genModulePreamble函數(shù)后,已經(jīng)生成了一條import {xxx} from "vue"了。我們接著來看generate函數(shù)中render函數(shù)的函數(shù)名稱和參數(shù)是如何生成的,代碼如下:

    const functionName = `render`;
    const args = ["_ctx""_cache"];
    args.push("$props""$setup""$data""$options");
    const signature = args.join(", ");
    push(`function ${functionName}(${signature}) {`);

    上面的代碼很簡單,都是執(zhí)行push方法向render函數(shù)中添加code字符串,其中args數(shù)組就是render函數(shù)中的參數(shù)。我們在來看看執(zhí)行完上面這塊代碼后的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到此時(shí)已經(jīng)生成了render函數(shù)中的函數(shù)名稱和參數(shù)了。

    生成render函數(shù)中return的內(nèi)容

    接著來看generate函數(shù)中最后一塊代碼,如下:

    indent();
    push(`return `);
    genNode(ast.codegenNode, context);

    首先調(diào)用indent方法插入一個(gè)換行符并且增加一個(gè)鎖進(jìn),然后執(zhí)行push方法添加一個(gè)return字符串。

    接著以根節(jié)點(diǎn)的codegenNode屬性為參數(shù)執(zhí)行genNode函數(shù)生成return中的內(nèi)容,在我們這個(gè)場景中genNode函數(shù)簡化后的代碼如下:

    function genNode(node, context) {
      switch (node.type) {
        case NodeTypes.SIMPLE_EXPRESSION:
          genExpression(node, context)
          break
        case NodeTypes.INTERPOLATION:
          genInterpolation(node, context);
          break;
        case NodeTypes.VNODE_CALL:
          genVNodeCall(node, context);
          break;
      }
    }

    這里涉及到SIMPLE_EXPRESSION、INTERPOLATIONVNODE_CALL三種AST抽象語法樹node節(jié)點(diǎn)類型:

    • INTERPOLATION:表示當(dāng)前節(jié)點(diǎn)是雙大括號節(jié)點(diǎn),我們這個(gè)demo中就是:{{msg}}這個(gè)文本節(jié)點(diǎn)。

    • SIMPLE_EXPRESSION:表示當(dāng)前節(jié)點(diǎn)是簡單表達(dá)式節(jié)點(diǎn),在我們這個(gè)demo中就是雙大括號節(jié)點(diǎn){{msg}}中的更里層節(jié)點(diǎn)msg

    • VNODE_CALL:表示當(dāng)前節(jié)點(diǎn)是虛擬節(jié)點(diǎn),比如我們這里第一次調(diào)用genNode函數(shù)傳入的ast.codegenNode(根節(jié)點(diǎn)的codegenNode屬性)就是虛擬節(jié)點(diǎn)。

    genVNodeCall函數(shù)

    由于當(dāng)前節(jié)點(diǎn)是虛擬節(jié)點(diǎn),第一次進(jìn)入genNode函數(shù)時(shí)會(huì)執(zhí)行genVNodeCall函數(shù)。在我們這個(gè)場景中簡化后的genVNodeCall函數(shù)代碼如下:

    const OPEN_BLOCK = Symbol(`openBlock`);
    const CREATE_ELEMENT_BLOCK = Symbol(`createElementBlock`);

    function genVNodeCall(node, context) {
      const { push, helper } = context;
      const { tag, props, children, patchFlag, dynamicProps, isBlock } = node;
      if (isBlock) {
        push(`(${helper(OPEN_BLOCK)}(${``}), `);
      }
      const callHelper = CREATE_ELEMENT_BLOCK;
      push(helper(callHelper) + `(`, -2 /* None */, node);

      genNodeList(
        // 將參數(shù)中的undefined轉(zhuǎn)換成null
        genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
        context
      );

      push(`)`);
      if (isBlock) {
        push(`)`);
      }
    }

    首先判斷當(dāng)前節(jié)點(diǎn)是不是block節(jié)點(diǎn),由于此時(shí)的node為根節(jié)點(diǎn),所以isBlock為true。將斷點(diǎn)走進(jìn)helper方法,我們來看看helper(OPEN_BLOCK)返回值是什么。helper方法的代碼如下:

    const helperNameMap = {
      [OPEN_BLOCK]: `openBlock`,
      [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
      [TO_DISPLAY_STRING]: `toDisplayString`,
      // ...省略
    };

    helper(key) {
      return `_${helperNameMap[key]}`;
    }

    helper方法中的代碼很簡單,這里的helper(OPEN_BLOCK)返回的就是_openBlock。

    將斷點(diǎn)走到第一個(gè)push方法,代碼如下:

    push(`(${helper(OPEN_BLOCK)}(${``}), `);

    執(zhí)行完這個(gè)push方法后在debug終端看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到,此時(shí)render函數(shù)中增加了一個(gè)_openBlock函數(shù)的調(diào)用。

    將斷點(diǎn)走到第二個(gè)push方法,代碼如下:

    const callHelper = CREATE_ELEMENT_BLOCK;
    push(helper(callHelper) + `(`, -2 /* None */, node);

    同理helper(callHelper)方法返回的是_createElementBlock,執(zhí)行完這個(gè)push方法后在debug終端看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到,此時(shí)render函數(shù)中增加了一個(gè)_createElementBlock函數(shù)的調(diào)用。

    繼續(xù)將斷點(diǎn)走到genNodeList部分,代碼如下:

    genNodeList(
      genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
      context
    );

    其中的genNullableArgs函數(shù)功能很簡單,將參數(shù)中的undefined轉(zhuǎn)換成null。比如此時(shí)的props就是undefined,經(jīng)過genNullableArgs函數(shù)處理后傳給genNodeList函數(shù)的props就是null。

    genNodeList函數(shù)

    繼續(xù)將斷點(diǎn)走進(jìn)genNodeList函數(shù),在我們這個(gè)場景中簡化后的代碼如下:

    function genNodeList(nodes, context, multilines = false, comma = true) {
      const { push } = context;
      for (let i = 0; i < nodes.length; i++) {
        const node = nodes[i];
        if (shared.isString(node)) {
          push(node);
        } else {
          genNode(node, context);
        }
        if (i < nodes.length - 1) {
          comma && push(", ");
        }
      }
    }

    我們先來看看此時(shí)的nodes參數(shù),如下圖:

    這里的nodes就是調(diào)用genNodeList函數(shù)時(shí)傳的數(shù)組:[tag, props, children, patchFlag, dynamicProps],只是將數(shù)組中的undefined轉(zhuǎn)換成了null

    • nodes數(shù)組中的第一項(xiàng)為字符串p,表示當(dāng)前節(jié)點(diǎn)是p標(biāo)簽。

    • 由于當(dāng)前p標(biāo)簽沒有props,所以第二項(xiàng)為null的字符串。

    • 第三項(xiàng)為p標(biāo)簽子節(jié)點(diǎn):{{msg}}

    • 第四項(xiàng)也是一個(gè)字符串,標(biāo)記當(dāng)前節(jié)點(diǎn)是否是動(dòng)態(tài)節(jié)點(diǎn)。

    在講genNodeList函數(shù)之前,我們先來看一下如何使用h函數(shù)生成一個(gè)<p>{{ msg }}</p>標(biāo)簽的虛擬DOM節(jié)點(diǎn)。根據(jù)vue官網(wǎng)的介紹,h函數(shù)定義如下:

    // 完整參數(shù)簽名
    function h(
      type: string | Component,
      props?: object | null,
      children?: Children | Slot | Slots
    ): VNode

    h函數(shù)接收的第一個(gè)參數(shù)是標(biāo)簽名稱或者一個(gè)組件,第二個(gè)參數(shù)是props對象或者null,第三個(gè)參數(shù)是子節(jié)點(diǎn)。

    所以我們要使用h函數(shù)生成demo中的p標(biāo)簽虛擬DOM節(jié)點(diǎn)代碼如下:

    h("p", null, msg)

    h函數(shù)生成虛擬DOM實(shí)際就是調(diào)用的createBaseVNode函數(shù),而我們這里的createElementBlock函數(shù)生成虛擬DOM也是調(diào)用的createBaseVNode函數(shù)。兩者的區(qū)別是createElementBlock函數(shù)多接收一些參數(shù),比如patchFlagdynamicProps

    現(xiàn)在我想你應(yīng)該已經(jīng)反應(yīng)過來了,為什么調(diào)用genNodeList函數(shù)時(shí)傳入的第一個(gè)參數(shù)nodes為:[tag, props, children, patchFlag, dynamicProps]。這個(gè)數(shù)組的順序就是調(diào)用createElementBlock函數(shù)時(shí)傳入的參數(shù)順序。

    所以在genNodeList中會(huì)遍歷nodes數(shù)組生成調(diào)用createElementBlock函數(shù)需要傳入的參數(shù)。

    先來看第一個(gè)參數(shù)tag,這里tag的值為字符串"p"。所以在for循環(huán)中會(huì)執(zhí)行push(node),生成調(diào)用createElementBlock函數(shù)的第一個(gè)參數(shù)"p"。在debug終端看看此時(shí)的render函數(shù),如下圖:

    從上圖中可以看到createElementBlock函數(shù)的第一個(gè)參數(shù)"p"

    接著來看nodes數(shù)組中的第二個(gè)參數(shù):props,由于p標(biāo)簽中沒有props屬性。所以第二個(gè)參數(shù)props的值為字符串"null",在for循環(huán)中同樣會(huì)執(zhí)行push(node),生成調(diào)用createElementBlock函數(shù)的第二個(gè)參數(shù)"null"。在debug終端看看此時(shí)的render函數(shù),如下圖:

    從上圖中可以看到createElementBlock函數(shù)的第二個(gè)參數(shù)null

    接著來看nodes數(shù)組中的第三個(gè)參數(shù):children,由于children是一個(gè)對象,所以以當(dāng)前children節(jié)點(diǎn)作為參數(shù)執(zhí)行genNode函數(shù)。

    這個(gè)genNode函數(shù)前面已經(jīng)執(zhí)行過一次了,當(dāng)時(shí)是以根節(jié)點(diǎn)的codegenNode屬性作為參數(shù)執(zhí)行的?;仡櫼幌?code style="color: rgb(30, 107, 184);font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;margin-left: 2px;margin-right: 2px;padding: 2px 4px;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;">genNode函數(shù)的代碼,如下:

    function genNode(node, context) {
      switch (node.type) {
        case NodeTypes.SIMPLE_EXPRESSION:
          genExpression(node, context)
          break
        case NodeTypes.INTERPOLATION:
          genInterpolation(node, context);
          break;
        case NodeTypes.VNODE_CALL:
          genVNodeCall(node, context);
          break;
      }
    }

    前面我們講過了NodeTypes.INTERPOLATION類型表示當(dāng)前節(jié)點(diǎn)是雙大括號節(jié)點(diǎn),而我們這次執(zhí)行genNode函數(shù)傳入的p標(biāo)簽children,剛好就是{{msg}}雙大括號節(jié)點(diǎn)。所以代碼會(huì)走到genInterpolation函數(shù)中。

    genInterpolation函數(shù)

    將斷點(diǎn)走進(jìn)genInterpolation函數(shù)中,genInterpolation代碼如下:

    function genInterpolation(node, context) {
      const { push, helper } = context;
      push(`${helper(TO_DISPLAY_STRING)}(`);
      genNode(node.content, context);
      push(`)`);
    }

    首先會(huì)執(zhí)行push方法向render函數(shù)中插入一個(gè)_toDisplayString函數(shù)調(diào)用,在debug終端看看執(zhí)行完這個(gè)push方法后的render函數(shù),如下圖:

    從上圖中可以看到此時(shí)createElementBlock函數(shù)的第三個(gè)參數(shù)只生成了一半,調(diào)用_toDisplayString函數(shù)傳入的參數(shù)還沒生成。

    接著會(huì)以node.content作為參數(shù)執(zhí)行genNode(node.content, context);生成_toDisplayString函數(shù)的參數(shù),此時(shí)代碼又走回了genNode函數(shù)。

    將斷點(diǎn)再次走進(jìn)genNode函數(shù),看看此時(shí)的node是什么樣的,如下圖:

    從上圖中可以看到此時(shí)的node節(jié)點(diǎn)是一個(gè)簡單表達(dá)式節(jié)點(diǎn),表達(dá)式為:$setup.msg。所以代碼會(huì)走進(jìn)genExpression函數(shù)。

    genExpression函數(shù)

    接著將斷點(diǎn)走進(jìn)genExpression函數(shù)中,genExpression函數(shù)中的代碼如下:

    function genExpression(node, context) {
      const { content, isStatic } = node;
      context.push(
        isStatic ? JSON.stringify(content) : content,
        -3 /* Unknown */,
        node
      );
    }

    由于當(dāng)前的msg變量是一個(gè)ref響應(yīng)式變量,所以isStaticfalse。所以會(huì)執(zhí)行push方法,將$setup.msg插入到render函數(shù)中。

    執(zhí)行完push方法后,在debug終端看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到此時(shí)的render函數(shù)基本已經(jīng)生成了,剩下的就是調(diào)用push方法生成各個(gè)函數(shù)的右括號")"和右花括號"}"。將斷點(diǎn)逐層走出,直到generate函數(shù)中。代碼如下:

    function generate(ast) {
      // ...省略
      genNode(ast.codegenNode, context);

      deindent();
      push(`}`);
      return {
        ast,
        code: context.code,
      };
    }

    執(zhí)行完最后一個(gè)  push方法后,在debug終端看看此時(shí)的render函數(shù)字符串是什么樣的,如下圖:

    從上圖中可以看到此時(shí)的render函數(shù)終于生成啦!

    總結(jié)

    這是我畫的我們這個(gè)場景中generate生成render函數(shù)的流程圖:


    • 執(zhí)行genModulePreamble函數(shù)生成:import { xxx } from "vue";

    • 簡單字符串拼接生成render函數(shù)中的函數(shù)名稱和參數(shù),也就是function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {

    • 以根節(jié)點(diǎn)的codegenNode屬性為參數(shù)調(diào)用genNode函數(shù)生成render函數(shù)中return的內(nèi)容。

      • 此時(shí)傳入的是虛擬節(jié)點(diǎn),執(zhí)行genVNodeCall函數(shù)生成return _openBlock(), _createElementBlock(和調(diào)用genNodeList函數(shù),生成createElementBlock函數(shù)的參數(shù)。

      • 處理p標(biāo)簽的tag標(biāo)簽名和props,生成createElementBlock函數(shù)的第一個(gè)和第二個(gè)參數(shù)。此時(shí)render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null

      • 處理p標(biāo)簽的children也就是{{msg}}節(jié)點(diǎn),再次調(diào)用genNode函數(shù)。此時(shí)node節(jié)點(diǎn)類型為雙大括號節(jié)點(diǎn),調(diào)用genInterpolation函數(shù)。

      • genInterpolation函數(shù)中會(huì)先調(diào)用push方法,此時(shí)的render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString(。然后以node.content為參數(shù)再次調(diào)用genNode函數(shù)。

      • node.content$setup.msg,是一個(gè)簡單表達(dá)式節(jié)點(diǎn),所以在genNode函數(shù)中會(huì)調(diào)用genExpression函數(shù)。執(zhí)行完genExpression函數(shù)后,此時(shí)的render函數(shù)return的內(nèi)容為:return _openBlock(), _createElementBlock("p", null, _toDisplayString($setup.msg

      • 調(diào)用push方法生成各個(gè)函數(shù)的右括號")"和右花括號"}",生成最終的render函數(shù)



    瀏覽 42
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評論
    圖片
    表情
    推薦
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    久久精品国产青青草 | 91美女禁| 久久久久草 | 人人搞人人操 | 亚洲毛多水多 |