徹底搞清楚vue3的defineExpose宏函式是如何暴露方法給父元件使用

前端欧阳發表於2024-05-29

前言

眾所周知,當子元件使用setup後,父元件就不能像vue2那樣直接就可以訪問子元件內的屬性和方法。這個時候就需要在子元件內使用defineExpose宏函式來指定想要暴露出去的屬性和方法。這篇文章來講講defineExpose宏函式是如何暴露出去這些屬性和方法給父元件使用。注:本文中使用的vue版本為3.4.19

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

加我微信heavenyjj0012回覆「666」,免費領取歐陽研究vue原始碼過程中收集的原始碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。

看個demo

父元件index.vue的程式碼如下:

<template>
  <ChildDemo ref="child" />
  <button @click="handleClick">呼叫子元件的validate方法</button>
</template>

<script setup lang="ts">
import ChildDemo from "./child.vue";
import { ref } from "vue";

const child = ref();

function handleClick() {
  console.log(child.value.validate);
  child.value.validate?.();
}
</script>

上面的程式碼很簡單,透過ref拿到子元件的例項賦值給child變數。然後在按鈕的click事件中列印出子元件的validate方法和執行validate方法。

再來看看子元件child.vue不使用defineExpose宏的例子,程式碼如下:

<template></template>

<script setup>
function validate() {
  console.log("執行子元件validate方法");
}
</script>

在瀏覽器中點選父元件的button按鈕,可以看到控制檯中列印的是undefined,並且子元件內的validate方法也沒有執行。因為子元件使用了setup,預設是不會暴露setup中定義的屬性和方法。如下圖:
no-defineExpose

我們再來看看子元件child.vue使用defineExpose宏的例子,程式碼如下:

<template></template>

<script setup>
function validate() {
  console.log("執行子元件validate方法");
}

defineExpose({
  validate,
});
</script>

在瀏覽器中點選父元件的button按鈕,可以看到控制檯中列印的不再是undefined,子元件內的validate方法也執行了。如下圖:
has-defineExpose

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

加我微信heavenyjj0012回覆「666」,免費領取歐陽研究vue原始碼過程中收集的原始碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。

編譯後的程式碼

首先需要在瀏覽器中找到編譯後的使用defineExpose宏的child.vue檔案,在network皮膚中找到child.vue,然後右鍵點選Open in Sources panel就可以在source皮膚中找到編譯後的child.vue。如下圖:
network

為了要在瀏覽器中debug,我們還需要在設定中關閉瀏覽器的javascript source map,如下圖:
source-map

現在我們來看看編譯後的child.vue檔案,程式碼如下:

const _sfc_main = {
  __name: "child",
  setup(__props, { expose: __expose }) {
    function validate() {
      console.log("執行子元件validate方法");
    }
    __expose({
      validate,
    });
    const __returned__ = { validate };
    return __returned__;
  },
};

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

從上面可以看到_sfc_main物件中的setup對應的就是我們原始碼<script setup>中的內容,並且defineExpose宏函式也不在了,變成了一個__expose方法(defineExpose宏函式如何編譯成__expose方法我們會在下一篇文章講)。如下圖:
convert

expose方法

__expose方法打個斷點,重新整理頁面此時斷點停留在__expose方法上面。點選step into進入到__expose方法內部,如下圖:
step-into

進入到__expose方法內部,我們發現__expose方法是在一個createSetupContext函式中定義的。在我們這個場景中createSetupContext函式簡化後的程式碼如下:

function createSetupContext(instance) {
  const expose = (exposed) => {
    instance.exposed = exposed || {};
  };

  return Object.freeze({
    // ...省略
    expose,
  });
}

我們先來看看函式中的instance變數,我想你透過名字應該已經猜到了他就是當前vue例項物件。如下圖:
instance

在vue例項物件中有我們熟悉的data方法、directives和componens屬性等。

expose函式內部做的事情也很簡單,將子元件需要暴露的屬性或者方法組成的物件賦值給vue例項上的exposed屬性。

父元件訪問子元件的validate方法

在vue3中想要訪問子元件需要使用特殊的 ref attribute,在我們這個例子中就是使用<ChildDemo ref="child" />。這樣使用後就可以使用child變數訪問子元件,其實在這裡child變數的值就是一個名為getExposeProxy函式的返回值(後面的文章中會去詳細講解ref attribute是如何訪問子元件)。

getExposeProxy函式的程式碼如下:

function getExposeProxy(instance) {
  if (instance.exposed) {
    return (
      instance.exposeProxy ||
      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
        get(target, key) {
          if (key in target) {
            return target[key];
          } else if (key in publicPropertiesMap) {
            return publicPropertiesMap[key](instance);
          }
        },
        has(target, key) {
          // ...省略
        },
      }))
    );
  }
}

前面我們講過了defineExpose宏函式中定義了想要暴露出來的屬性和方法,經過編譯後defineExpose宏函式變成了__expose方法。執行__expose方法後會將子元件想要暴露的屬性或者方法組成的物件賦值給vue例項上的exposed屬性,也就是instance.exposed

在上面的getExposeProxy函式中就是返回了instance.exposedProxy物件,當我們使用child.value.validate訪問子元件的validate方法,其實就是訪問的是instance.exposed物件中的validate方法,而instance.exposed中的validate方法就是defineExpose宏函式暴露的validate方法。如下圖:
full-progress

總結

父元件想要訪問子元件暴露的validate方法主要分為下面四步:

  • 子元件使用defineExpose宏函式宣告想要暴露validate方法。

  • defineExpose宏函式經過編譯後變成__expose方法。

  • 執行__expose方法將子元件需要暴露的屬性或者方法組成的物件賦值給子元件vue例項上的exposed屬性,也就是instance.exposed

  • 父元件使用ref訪問子元件的validate方法,也就是訪問child.value.validate。其實訪問的就是上一步的instance.exposed.validate方法,最終訪問的就是defineExpose宏函式中暴露的validate方法。

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

加我微信heavenyjj0012回覆「666」,免費領取歐陽研究vue原始碼過程中收集的原始碼資料,歐陽寫文章有時也會參考這些資料。同時讓你的朋友圈多一位對vue有深入理解的人。

相關文章