有點東西,template可以直接使用setup語法糖中的變數原來是因為這個

来源:https://www.cnblogs.com/heavenYJJ/p/18245547
-Advertisement-
Play Games

你知道為什麼setup語法糖中的頂層綁定可以在template中直接使用的呢?setup語法糖是如何編譯成setup函數的呢? ...


前言

我們每天寫vue3代碼的時候都會使用到setup語法糖,那你知道為什麼setup語法糖中的頂層綁定可以在template中直接使用的呢?setup語法糖是如何編譯成setup函數的呢?本文將圍繞這些問題帶你揭開setup語法糖的神秘面紗。註:本文中使用的vue版本為3.4.19

關註公眾號:【前端歐陽】,給自己一個進階vue的機會

看個demo

看個簡單的demo,代碼如下:

<template>
  <h1>{{ msg }}</h1>
  <h2>{{ format(msg) }}</h2>
  <h3>{{ title }}</h3>
  <Child />
</template>

<script lang="ts" setup>
import { ref } from "vue";
import Child from "./child.vue";
import { format } from "./util.js";

const msg = ref("Hello World!");

let title;

if (msg.value) {
  const innerContent = "xxx";
  console.log(innerContent);
  title = "111";
} else {
  title = "222";
}
</script>

在上面的demo中定義了四個頂層綁定:Child子組件、從util.js文件中導入的format方法、使用ref定義的msg只讀常量、使用let定義的title變數。並且在template中直接使用了這四個頂層綁定。

由於innerContent是在if語句裡面的變數,不是<script setup>中的頂層綁定,所以在template中是不能使用innerContent的。

但是你有沒有想過為什麼<script setup>中的頂層綁定就能在template中使用,而像innerContent這種非頂層綁定就不能在template中使用呢?

我們先來看看上面的代碼編譯後的樣子,在之前的文章中已經講過很多次如何在瀏覽器中查看編譯後的vue文件,這篇文章就不贅述了。編譯後的代碼如下:

import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import { ref } from "/node_modules/.vite/deps/vue.js?v=23bfe016";
import Child from "/src/components/setupDemo2/child.vue";
import { format } from "/src/components/setupDemo2/util.js";

const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    __expose();
    const msg = ref("Hello World!");
    let title;
    if (msg.value) {
      const innerContent = "xxx";
      console.log(innerContent);
      title = "111";
    } else {
      title = "222";
    }
    const __returned__ = {
      msg,
      get title() {
        return title;
      },
      set title(v) {
        title = v;
      },
      Child,
      get format() {
        return format;
      },
    };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  // ...省略
}
_sfc_main.render = _sfc_render;
export default _sfc_main;

從上面的代碼中可以看到編譯後已經沒有了<script setup>,取而代之的是一個setup函數,這也就證明瞭為什麼說setup是一個編譯時語法糖。

setup函數的參數有兩個,第一個參數為組件的 props。第二個參數為Setup 上下文對象,上下文對象暴露了其他一些在 setup 中可能會用到的值,比如:expose等。

再來看看setup函數中的內容,其實和我們的源代碼差不多,只是多了一個return。使用return會將組件中的那四個頂層綁定暴露出去,所以在template中就可以直接使用<script setup>中的頂層綁定。

值的一提的是在return對象中title變數和format函數有點特別。titleformat這兩個都是屬於訪問器屬性,其他兩個msgChild屬於常見的數據屬性。

title是一個訪問器屬性,同時擁有get 和 set,讀取title變數時會走進get中,當給title變數賦值時會走進set中。

format也是一個訪問器屬性,他只擁有get ,調用format函數時會走進get中。由於他沒有set,所以不能給format函數重新賦值。其實這個也很容易理解,因為format函數是從util.js文件中import導入的,當然不能給他重新賦值。

至於在template中是怎麼拿到setup函數返回的對象可以看我的另外一篇文章: Vue 3 的 setup語法糖到底是什麼東西?

看到這裡有的小伙伴會有疑問了,不是還有一句import { ref } from "vue"也是頂層綁定,為什麼裡面的ref沒有在setup函數中使用return暴露出去呢?還有在return對象中是如何將titleformat識別為訪問器屬性呢?

在接下來的文章中我會逐一解答這些問題。

compileScript函數

在之前的 通過debug搞清楚.vue文件怎麼變成.js文件文章中已經講過了vue的script模塊中的內容是由@vue/compiler-sfc包中的compileScript函數處理的,當然你沒看過那篇文章也不會影響這篇文章的閱讀。

首先我們需要啟動一個debug終端。這裡以vscode舉例,打開終端然後點擊終端中的+號旁邊的下拉箭頭,在下拉中點擊Javascript Debug Terminal就可以啟動一個debug終端。
debug-terminal

然後在node_modules中找到vue/compiler-sfc包的compileScript函數打上斷點,compileScript函數位置在/node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js。接下來我們先看看簡化後的compileScript函數源碼。

簡化後的compileScript函數

debug終端上面執行yarn dev後在瀏覽器中打開對應的頁面,比如:http://localhost:5173/ 。此時斷點就會走到compileScript函數中,在我們這個場景中簡化後的compileScript函數代碼如下:

function compileScript(sfc, options) {
  // ---- 第一部分 ----
  // 根據<script setup>中的內容生成一個ctx上下文對象
  // 在ctx上下文對象中擁有一些屬性和方法
  const ctx = new ScriptCompileContext(sfc, options);
  const { source, filename } = sfc;
  // 頂層聲明的變數、函數組成的對象
  const setupBindings = Object.create(null);
  // script標簽中的內容開始位置和結束位置
  const startOffset = ctx.startOffset;
  const endOffset = ctx.endOffset;
  // script setup中的內容編譯成的AST抽象語法樹
  const scriptSetupAst = ctx.scriptSetupAst;

  // ---- 第二部分 ----
  // 遍歷<script setup>中的內容,處理裡面的import語句、頂層變數、函數、類、枚舉聲明還有巨集函數
  for (const node of scriptSetupAst.body) {
    if (node.type === "ImportDeclaration") {
      // ...省略
    }
  }
  for (const node of scriptSetupAst.body) {
    if (
      (node.type === "VariableDeclaration" ||
        node.type === "FunctionDeclaration" ||
        node.type === "ClassDeclaration" ||
        node.type === "TSEnumDeclaration") &&
      !node.declare
    ) {
      // 頂層聲明的變數、函數、類、枚舉聲明組成的setupBindings對象
      // 給setupBindings對象賦值,{msg: 'setup-ref'}
      // 頂層聲明的變數組成的setupBindings對象
      walkDeclaration(
        "scriptSetup",
        node,
        setupBindings,
        vueImportAliases,
        hoistStatic
      );
    }
  }

  // ---- 第三部分 ----
  // 移除template中的內容和script的開始標簽
  ctx.s.remove(0, startOffset);
  // 移除style中的內容和script的結束標簽
  ctx.s.remove(endOffset, source.length);

  // ---- 第四部分 ----
  // 將<script setup>中的頂層綁定的元數據存儲到ctx.bindingMetadata對象中
  // 為什麼要多此一舉存儲一個bindingMetadata對象呢?答案是setup的return的對象有時會直接返回頂層變數,有時會返回變數的get方法,有時會返回變數的get和set方法,
  // 所以才需要一個bindingMetadata對象來存儲這些頂層綁定的元數據。
  for (const [key, { isType, imported, source: source2 }] of Object.entries(
    ctx.userImports
  )) {
    if (isType) continue;
    ctx.bindingMetadata[key] =
      imported === "*" ||
      (imported === "default" && source2.endsWith(".vue")) ||
      source2 === "vue"
        ? "setup-const"
        : "setup-maybe-ref";
  }
  for (const key in setupBindings) {
    ctx.bindingMetadata[key] = setupBindings[key];
  }
  // 生成setup方法的args參數;
  let args = `__props`;
  const destructureElements =
    ctx.hasDefineExposeCall || !options.inlineTemplate
      ? [`expose: __expose`]
      : [];
  if (destructureElements.length) {
    args += `, { ${destructureElements.join(", ")} }`;
  }

  // ---- 第五部分 ----
  // 根據<script setup>中的頂層綁定生成return對象中的內容
  let returned;
  const allBindings = {
    ...setupBindings,
  };
  for (const key in ctx.userImports) {
    // 不是引入ts中的類型並且import導入的變數還需要在template中使用
    if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {
      allBindings[key] = true;
    }
  }
  returned = `{ `;
  for (const key in allBindings) {
    if (
      allBindings[key] === true &&
      ctx.userImports[key].source !== "vue" &&
      !ctx.userImports[key].source.endsWith(".vue")
    ) {
      returned += `get ${key}() { return ${key} }, `;
    } else if (ctx.bindingMetadata[key] === "setup-let") {
      const setArg = key === "v" ? `_v` : `v`;
      returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
    } else {
      returned += `${key}, `;
    }
  }
  returned = returned.replace(/, $/, "") + ` }`;
  ctx.s.appendRight(
    endOffset,
    `
const __returned__ = ${returned}
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
`
  );

  // ---- 第六部分 ----
  // 生成setup函數
  ctx.s.prependLeft(
    startOffset,
    `
${genDefaultAs} /*#__PURE__*/${ctx.helper(
      `defineComponent`
    )}({${def}${runtimeOptions}
${hasAwait ? `async ` : ``}setup(${args}) {
${exposeCall}`
  );
  ctx.s.appendRight(endOffset, `})`);

  // ---- 第七部分 ----
  // 插入import vue語句
  if (ctx.helperImports.size > 0) {
    ctx.s.prepend(
      `import { ${[...ctx.helperImports]
        .map((h) => `${h} as _${h}`)
        .join(", ")} } from 'vue'
`
    );
  }

  return {
    // ...省略
    bindings: ctx.bindingMetadata,
    imports: ctx.userImports,
    content: ctx.s.toString(),
  };
}

首先我們來看看compileScript函數的第一個參數sfc對象,在之前的文章 vue文件是如何編譯為js文件 中我們已經講過了sfc是一個descriptor對象,descriptor對象是由vue文件編譯來的。

descriptor對象擁有template屬性、scriptSetup屬性、style屬性,分別對應vue文件的<template>模塊、<script setup>模塊、<style>模塊。

在我們這個場景只關註scriptSetup屬性,sfc.scriptSetup.content的值就是<script setup>模塊中code代碼字元串,

sfc.source的值就是vue文件中的源代碼code字元串。sfc.scriptSetup.loc.start.offset<script setup>中內容開始位置,sfc.scriptSetup.loc.end.offset<script setup>中內容結束位置。詳情查看下圖:
sfc

我們再來看compileScript函數中的內容,在compileScript函數中包含了從<script setup>語法糖到setup函數的完整流程。乍一看可能比較難以理解,所以我將其分為七塊。

  • 根據<script setup>中的內容生成一個ctx上下文對象。

  • 遍歷<script setup>中的內容,處理裡面的import語句、頂層變數、頂層函數、頂層類、頂層枚舉聲明等。

  • 移除template和style中的內容,以及script的開始標簽和結束標簽。

  • <script setup>中的頂層綁定的元數據存儲到ctx.bindingMetadata對象中。

  • 根據<script setup>中的頂層綁定生成return對象。

  • 生成setup函數定義

  • 插入import vue語句

在接下來的文章中我將逐個分析這七塊的內容。

生成ctx上下文對象

我們來看第一塊的代碼,如下:

// 根據<script setup>中的內容生成一個ctx上下文對象
// 在ctx上下文對象中擁有一些屬性和方法
const ctx = new ScriptCompileContext(sfc, options);
const { source, filename } = sfc;
// 頂層聲明的變數、函數組成的對象
const setupBindings = Object.create(null);
// script標簽中的內容開始位置和結束位置
const startOffset = ctx.startOffset;
const endOffset = ctx.endOffset;
// script setup中的內容編譯成的AST抽象語法樹
const scriptSetupAst = ctx.scriptSetupAst;

在這一塊的代碼中主要做了一件事,使用ScriptCompileContext構造函數new了一個ctx上下文對象。在之前的 為什麼defineProps巨集函數不需要從vue中import導入?文章中我們已經講過了ScriptCompileContext構造函數裡面的具體代碼,這篇文章就不贅述了。

本文只會講用到的ScriptCompileContext類中的startOffsetendOffsetscriptSetupAstuserImportshelperImportsbindingMetadatas等屬性。

  • startOffsetendOffset屬性是在ScriptCompileContext類的constructor構造函數中賦值的。其實就是sfc.scriptSetup.loc.start.offsetsfc.scriptSetup.loc.end.offset<script setup>中內容開始位置和<script setup>中內容結束位置,只是將這兩個欄位塞到ctx上下文中。

  • scriptSetupAst是在ScriptCompileContext類的constructor構造函數中賦值的,他是<script setup>模塊的代碼轉換成的AST抽象語法樹。在ScriptCompileContext類的constructor構造函數中會調用@babel/parser包的parse函數,以<script setup>中的code代碼字元串為參數生成AST抽象語法樹。

  • userImports在new一個ctx上下文對象時是一個空對象,用於存儲import導入的頂層綁定內容。

  • helperImports同樣在new一個ctx上下文對象時是一個空對象,用於存儲需要從vue中import導入的函數。

  • bindingMetadata同樣在new一個ctx上下文對象時是一個空對象,用於存儲所有的import頂層綁定和變數頂層綁定的元數據。

  • s屬性是在ScriptCompileContext類的constructor構造函數中賦值的,以vue文件中的源代碼code字元串為參數new了一個MagicString對象賦值給s屬性。

magic-string是由svelte的作者寫的一個庫,用於處理字元串的JavaScript庫。它可以讓你在字元串中進行插入、刪除、替換等操作,並且能夠生成準確的sourcemap

MagicString對象中擁有toStringremoveprependLeftappendRight等方法。s.toString用於生成返回的字元串,我們來舉幾個例子看看這幾個方法你就明白了。

s.remove( start, end )用於刪除從開始到結束的字元串:

const s = new MagicString('hello word');
s.remove(0, 6);
s.toString(); // 'word'

s.prependLeft( index, content )用於在指定index的前面插入字元串:

const s = new MagicString('hello word');
s.prependLeft(5, 'xx');
s.toString(); // 'helloxx word'

s.appendRight( index, content )用於在指定index的後面插入字元串:

const s = new MagicString('hello word');
s.appendRight(5, 'xx');
s.toString(); // 'helloxx word'

除了上面說的那幾個屬性,在這裡定義了一個setupBindings變數。初始值是一個空對象,用於存儲頂層聲明的變數、函數等。

遍歷<script setup>body中的內容

將斷點走到第二部分,代碼如下:

for (const node of scriptSetupAst.body) {
  if (node.type === "ImportDeclaration") {
    // ...省略
  }
}

for (const node of scriptSetupAst.body) {
  if (
    (node.type === "VariableDeclaration" ||
      node.type === "FunctionDeclaration" ||
      node.type === "ClassDeclaration" ||
      node.type === "TSEnumDeclaration") &&
    !node.declare
  ) {
    // 頂層聲明的變數、函數、類、枚舉聲明組成的setupBindings對象
    // 給setupBindings對象賦值,{msg: 'setup-ref'}
    // 頂層聲明的變數組成的setupBindings對象
    walkDeclaration(
      "scriptSetup",
      node,
      setupBindings,
      vueImportAliases,
      hoistStatic
    );
  }
}

在這一部分的代碼中使用for迴圈遍歷了兩次scriptSetupAst.bodyscriptSetupAst.body為script中的代碼對應的AST抽象語法樹中body的內容,如下圖:
body

從上圖中可以看到scriptSetupAst.body數組有6項,分別對應的是script模塊中的6塊代碼。

第一個for迴圈中使用if判斷node.type === "ImportDeclaration",也就是判斷是不是import語句。如果是import語句,那麼import的內容肯定是頂層綁定,需要將import導入的內容存儲到ctx.userImports對象中。註:後面會專門寫一篇文章來講如何收集所有的import導入。

通過這個for迴圈已經將所有的import導入收集到了ctx.userImports對象中了,在debug終端看看此時的ctx.userImports,如下圖:
userImports

從上圖中可以看到在ctx.userImports中收集了三個import導入,分別是Child組件、format函數、ref函數。

在裡面有幾個欄位需要註意,isUsedInTemplate表示當前import導入的東西是不是在template中使用,如果為true那麼就需要將這個import導入塞到return對象中。

isType表示當前import導入的是不是type類型,因為在ts中是可以使用import導入type類型,很明顯type類型也不需要塞到return對象中。

我們再來看第二個for迴圈,同樣也是遍歷scriptSetupAst.body。如果當前是變數定義、函數定義、類定義、ts枚舉定義,這四種類型都屬於頂層綁定(除了import導入以外就只有這四種頂層綁定了)。需要調用walkDeclaration函數將這四種頂層綁定收集到setupBindings對象中。

從前面的scriptSetupAst.body圖中可以看到if模塊的type為IfStatement,明顯不屬於上面的這四種類型,所以不會執行walkDeclaration函數將裡面的innerContent變數收集起來後面再塞到return對象中。這也就解釋了為什麼非頂層綁定不能在template中直接使用。

我們在debug終端來看看執行完第二個for迴圈後setupBindings對象是什麼樣的,如下圖:
setupBindings

從上圖中可以看到在setupBindings對象中收集msgtitle這兩個頂層變數。其中的setup-ref表示當前變數是一個ref定義的變數,setup-let表示當前變數是一個let定義的變數。

移除template模塊和style模塊

接著將斷點走到第三部分,代碼如下:

ctx.s.remove(0, startOffset);
ctx.s.remove(endOffset, source.length);

這塊代碼很簡單,startOffset<script setup>中的內容開始位置,endOffset<script setup>中的內容結束位置,ctx.s.remove方法為刪除字元串。

所以ctx.s.remove(0, startOffset)的作用是:移除template中的內容和script的開始標簽。

ctx.s.remove(endOffset, source.length)的作用是:移除style中的內容和script的結束標簽。

我們在debug終端看看執行這兩個remove方法之前的code代碼字元串是什麼樣的,如下圖:
before-remove

從上圖中可以看到此時的code代碼字元串和我們源代碼差不多,唯一的區別就是那幾個import導入已經被提取到script標簽外面去了(這個是在前面第一個for迴圈處理import導入的時候處理的)。

將斷點走到執行完這兩個remove方法之後,在debug終端看看此時的code代碼字元串,如下圖:
after-remove

從上圖中可以看到執行這兩個remove方法後template模塊、style模塊(雖然本文demo中沒有寫style模塊)、script開始標簽、script結束標簽都已經被刪除了。唯一剩下的就是script模塊中的內容,還有之前提出去的那幾個import導入。

將頂層綁定的元數據存儲到ctx.bindingMetadata

接著將斷點走到第四部分,代碼如下:

for (const [key, { isType, imported, source: source2 }] of Object.entries(
  ctx.userImports
)) {
  if (isType) continue;
  ctx.bindingMetadata[key] =
    imported === "*" ||
    (imported === "default" && source2.endsWith(".vue")) ||
    source2 === "vue"
      ? "setup-const"
      : "setup-maybe-ref";
}

for (const key in setupBindings) {
  ctx.bindingMetadata[key] = setupBindings[key];
}

// 生成setup函數的args參數;
let args = `__props`;
const destructureElements =
  ctx.hasDefineExposeCall || !options.inlineTemplate
    ? [`expose: __expose`]
    : [];
if (destructureElements.length) {
  args += `, { ${destructureElements.join(", ")} }`;
}

上面的代碼主要分為三塊,第一塊為for迴圈遍歷前面收集到的ctx.userImports對象。這個對象裡面收集的是所有的import導入,將所有import導入塞到ctx.bindingMetadata對象中。

第二塊也是for迴圈遍歷前面收集的setupBindings對象,這個對象裡面收集的是頂層聲明的變數、函數、類、枚舉,同樣的將這些頂層綁定塞到ctx.bindingMetadata對象中。

為什麼要多此一舉存儲一個ctx.bindingMetadata對象呢?

答案是setup的return的對象有時會直接返回頂層變數(比如demo中的msg常量)。有時只會返回變數的訪問器屬性 get(比如demo中的format函數)。有時會返回變數的訪問器屬性 get和set(比如demo中的title變數)。所以才需要一個ctx.bindingMetadata對象來存儲這些頂層綁定的元數據。

將斷點走到執行完這兩個for迴圈的地方,在debug終端來看看此時收集的ctx.bindingMetadata對象是什麼樣的,如下圖:
bindingMetadata

最後一塊代碼也很簡單進行字元串拼接生成setup函數的參數,第一個參數為組件的props、第二個參數為expose方法組成的對象。如下圖:
args

生成return對象

接著將斷點走到第五部分,代碼如下:

let returned;
const allBindings = {
  ...setupBindings,
};
for (const key in ctx.userImports) {
  // 不是引入ts中的類型並且import導入的變數還需要在template中使用
  if (!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate) {
    allBindings[key] = true;
  }
}

returned = `{ `;
for (const key in allBindings) {
  if (
    allBindings[key] === true &&
    ctx.userImports[key].source !== "vue" &&
    !ctx.userImports[key].source.endsWith(".vue")
  ) {
    returned += `get ${key}() { return ${key} }, `;
  } else if (ctx.bindingMetadata[key] === "setup-let") {
    const setArg = key === "v" ? `_v` : `v`;
    returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
  } else {
    returned += `${key}, `;
  }
}
returned = returned.replace(/, $/, "") + ` }`;

ctx.s.appendRight(
  endOffset,
  `
  const __returned__ = ${returned}
  Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
  return __returned__
  }
  `
);

這部分的代碼看著很多,其實邏輯也非常清晰,我也將其分為三塊。

在第一塊中首先使用擴展運算符...setupBindingssetupBindings對象中的屬性合併到allBindings對象中,因為setupBindings對象中存的頂層聲明的變數、函數、類、枚舉都需要被return出去。

然後遍歷ctx.userImports對象,前面講過了ctx.userImports對象中存的是所有的import導入(包括從vue中import導入ref函數)。在迴圈裡面執行了if判斷!ctx.userImports[key].isType && ctx.userImports[key].isUsedInTemplate,這個判斷的意思是如果當前import導入的不是ts的type類型並且import導入的內容在template模版中使用了。才會去執行allBindings[key] = true,執行後就會將滿足條件的import導入塞到allBindings對象中。

後面生成setup函數的return對象就是通過遍歷這個allBindings對象實現的。這也就解釋了為什麼從vue中import導入的ref函數也是頂層綁定,為什麼他沒有被setup函數返回。因為只有在template中使用的import導入頂層綁定才會被setup函數返回。

將斷點走到遍歷ctx.userImports對象之後,在debug終端來看看此時的allBindings對象是什麼樣的,如下圖:
allBindings

從上圖中可以看到此時的allBindings對象中存了四個需要return的頂層綁定。

接著就是執行for迴圈遍歷allBindings對象生成return對象的字元串,這迴圈中有三個if判斷條件。我們先來看第一個,代碼如下:

if (
  allBindings[key] === true &&
  ctx.userImports[key].source !== "vue" &&
  !ctx.userImports[key].source.endsWith(".vue")
) {
  returned += `get ${key}() { return ${key} }, `;
}

if條件判斷是:如果當前import導入不是從vue中,並且也不是import導入一個vue組件。那麼就給return一個只擁有get的訪問器屬性,對應我們demo中的就是import { format } from "./util.js"中的format函數。

我們再來看第二個else if判斷,代碼如下:

else if (ctx.bindingMetadata[key] === "setup-let") {
  const setArg = key === "v" ? `_v` : `v`;
  returned += `get ${key}() { return ${key} }, set ${key}(${setArg}) { ${key} = ${setArg} }, `;
}

這個else if條件判斷是:如果當前頂層綁定是一個let定義的變數。那麼就給return一個同時擁有get和set的訪問器屬性,對應我們demo中的就是let title"變數。

最後就是else,代碼如下:

else {
  returned += `${key}, `;
}

這個else中就是普通的數據屬性了,對應我們demo中的就是msg變數和Child組件。

將斷點走到生成return對象之後,在debug終端來看看此時生成的return對象是什麼樣的,如下圖:
returned

從上圖中可以看到此時已經生成了return對象啦。

前面我們只生成了return對象,但是還沒將其插入到要生成的code字元串中,所以需要執行ctx.s.appendRight方法在末尾插入return的代碼。

將斷點走到執行完ctx.s.appendRight方法後,在debug終端來看看此時的code代碼字元串是什麼樣的,如下圖:
after-returned

從上圖中可以看到此時的code代碼字元串中多了一塊return的代碼。

生成setup函數定義

接著將斷點走到第六部分,代碼如下:

ctx.s.prependLeft(
  startOffset,
  `
${genDefaultAs} /*#__PURE__*/${ctx.helper(
    `defineComponent`
  )}({${def}${runtimeOptions}
${hasAwait ? `async ` : ``}setup(${args}) {
${exposeCall}`
);
ctx.s.appendRight(endOffset, `})`);

這部分的代碼很簡單,調用ctx.s.prependLeft方法從左邊插入一串代碼。插入的這串代碼就是簡單的字元串拼接,我們在debug終端來看看要插入的代碼是什麼樣的,如下圖:
sfc-main

是不是覺得上面這塊需要插入的代碼看著很熟悉,他就是編譯後的_sfc_main對象除去setup函數內容的部分。將斷點走到ctx.s.appendRight方法執行之後,再來看看此時的code代碼字元串是什麼樣的,如下圖:
setup-fn

從上圖中可以看到此時的setup函數基本已經生成完了。

插入import vue語句

上一步生成的code代碼字元串其實還有一個問題,在代碼中使用了_defineComponent函數,但是沒有從任何地方去import導入。

第七塊的代碼就會生成缺少的import導入,代碼如下:

if (ctx.helperImports.size > 0) {
  ctx.s.prepend(
    `import { ${[...ctx.helperImports]
      .map((h) => `${h} as _${h}`)
      .join(", ")} } from 'vue'
`
  );
}

將斷點走到ctx.s.prepend函數執行後,再來看看此時的code代碼字元串,如下圖:
full-code

從上圖中可以看到已經生成了完整的setup函數啦。

總結

整個流程圖如下:
full-progress

  • 遍歷<script setup>中的代碼將所有的import導入收集到ctx.userImports對象中。

  • 遍歷<script setup>中的代碼將所有的頂層變數、函數、類、枚舉收集到setupBindings對象中。

  • 調用ctx.s.remove方法移除template、style模塊以及script開始標簽和結束標簽。

  • 遍歷前面收集的ctx.userImportssetupBindings對象,將所有的頂層綁定元數據存儲到bindingMetadata對象中。

  • 遍歷前面收集的ctx.userImportssetupBindings對象,生成return對象中的內容。在這一步的時候會將沒有在template中使用的import導入給過濾掉,這也就解釋了為什麼從vue中導入的ref函數不包含在return對象中。

  • 調用ctx.s.prependLeft方法生成setup的函數定義。

  • 調用ctx.s.prepend方法生成完整的setup函數。

關註公眾號:【前端歐陽】,給自己一個進階vue的機會


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 不使用臨時變數來交換變數的值 2. 對象解構,讓數據訪問更便捷 3. 淺克隆對象 4. 合併對象 5. 清理數組 6. 將 NodeList 轉換為數組 7. 檢查數組是否滿足指定條件 8. 將文本複製到剪貼板 9. 刪除數組重覆項 10. 取兩個數組的交集 11. 求數組元素的總和 12. ... ...
  • 這篇文章介紹了Web Components技術,它允許開發者創建可復用、封裝良好的自定義HTML元素,並直接在瀏覽器中運行,無需依賴外部庫。通過組合HTML模板、Shadow DOM、自定義元素和HTML imports,Web Components增強了原生DOM的功能,提高了組件化開發的封裝性和... ...
  • 隨便寫的小網頁練習: 原視頻:十分鐘學會寫網頁【編程前端入門】 簡介: 本期重做了好幾遍,是全新的視角和概念,從瀏覽器底層渲染原理到實現網站的演示,一節課講透,必看的一期。後面不管是寫小程式還是App,都會用到這一期的概念。 這一期內容是我做完第一章個人網站後更新的,用來替代原先沒做好的第三節, ...
  • 這篇文章介紹了微前端架構概念,聚焦於如何在Vue.js項目中應用Qiankun框架實現模塊化和組件化,以達到高效開發和維護的目的。討論了Qiankun的原理、如何設置主應用與子應用的通信,以及如何解決跨域問題和優化集成過程,從而實現前端應用的靈活擴展與組織。 ...
  • ‍ 寫在開頭 點贊 + 收藏 學會 在日常的開發過程中,我們都會有一些常用的代碼片段,這些代碼片段可以直接複製到各個項目中使用,非常方便。如果你有接手過別人的項目,就可以很明顯感受到幾個項目一般都會有一些相同的工具類方法,這些方法就是之前開發者的常用代碼片段。 現在前端社區相當完 ...
  • 這篇文章介紹瞭如何在Vue框架中實現自定義渲染器以增強組件功能,探討了虛擬DOM的工作原理,以及如何通過SSR和服務端預取數據優化首屏載入速度。同時,講解了同構應用的開發方式與狀態管理技巧,助力構建高性能前端應用。 ...
  • 應用場景: 實現目標: 在網頁端實現大文件(文件大小 >= 2 G) 斷點續傳 實際方案: 發送多次請求, 每次請求一部分文件數據, 然後通過續寫將文件數據全部寫入. 難點: 無法實現文件續寫, 最後採用 StreamSaver 來解決這個問題. 1. 首先從 git hub 將 StreamSav ...
  • SoybeanAdmin —— 一個清新優雅、高顏值且功能強大的後臺管理模板。基於最新的前端技術棧,包括 Vue3、 Vite5、 TypeScript、 Pinia 和 UnoCSS。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...