面試官:來談談Vue3的provide和inject實現多級傳遞的原理

前端欧阳發表於2024-12-03

前言

沒有看過provideinject函式原始碼的小夥伴可能覺得他們實現資料多級傳遞非常神秘,其實他的原始碼非常簡單,這篇文章歐陽來講講provideinject函式是如何實現資料多級傳遞的。ps:本文中使用的Vue版本為3.5.13

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

看個demo

先來看個demo,這個是父元件,程式碼如下:

<template>
  <ChildDemo />
</template>

<script setup>
import ChildDemo from "./child.vue";
import { ref, provide } from "vue";
// 提供響應式的值
const count = ref(0);
provide("count", count);
</script>

在父元件中使用provide為後代元件注入一個count響應式變數。

再來看看子元件child.vue程式碼如下:

<template>
  <GrandChild />
</template>
<script setup>
import GrandChild from "./grand-child.vue";
</script>

從上面的程式碼可以看到在子元件中什麼事情都沒做,只渲染了孫子元件。

我們再來看看孫子元件grand-child.vue,程式碼如下:

<script setup>
import { inject } from "vue";

// 注入響應式的值
const count = inject("count");
console.log("inject count is:", count);
</script>

從上面的程式碼可以看到在孫子元件中使用inject函式拿到了父元件中注入的count響應式變數。

provide函式

我們先來debug看看provide函式的程式碼,給父元件中的provide函式打個斷點,如下圖:
provide

重新整理頁面,此時程式碼將會停留在斷點處。讓斷點走進provide函式,程式碼如下:

function provide(key, value) {
  if (!currentInstance) {
    if (!!(process.env.NODE_ENV !== "production")) {
      warn$1(`provide() can only be used inside setup().`);
    }
  } else {
    let provides = currentInstance.provides;
    const parentProvides = currentInstance.parent && currentInstance.parent.provides;
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides);
    }
    provides[key] = value;
  }
}

首先判斷currentInstance是否有值,如果沒有就說明當前沒有vue例項,也就是說當前呼叫provide函式的地方是不在setup函式中執行的,然後給出警告provide只能在setup中使用。

然後走進else邏輯中,首先從當前vue例項中取出存的provides屬性物件。並且透過currentInstance.parent.provides拿到父元件vue例項中的provides屬性物件。

這裡為什麼需要判斷if (parentProvides === provides)呢?

因為在建立子元件時會預設使用父元件的provides屬性物件作為父元件的provides屬性物件。程式碼如下:

const instance: ComponentInternalInstance = {
  uid: uid++,
  vnode,
  type,
  parent,
  provides: parent ? parent.provides : Object.create(appContext.provides),
  // ...省略
}	

從上面的程式碼可以看到如果有父元件,那麼建立子元件例項的時候就直接使用父元件的provides屬性物件。

所以這裡在provide函式中需要判斷if (parentProvides === provides),如果相等說明當前父元件和子元件是共用的同一個provides屬性物件。此時如果子元件呼叫了provide函式,說明子元件需要建立自己的provides屬性物件。

並且新的屬性物件還需要能夠訪問到父元件中注入的內容,所以這裡以父元件的provides屬性物件為原型去建立一個新的子元件的,這樣在子元件中不僅能夠訪問到原型鏈中注入的provides屬性物件,也能夠訪問到自己注入進去的provides屬性物件。

最後就是執行provides[key] = value將當前注入的內容存到provides屬性物件中。

inject函式

我們再來看看inject函式是如何隔了一層子元件從父元件中如何取出資料的,還是一樣的套路,給孫子元件中的inject函式打個斷點。如下圖:
inject

將斷點走進inject函式,程式碼如下:

export function inject(
  key: InjectionKey<any> | string,
  defaultValue?: unknown,
  treatDefaultAsFactory = false,
) {
  // fallback to `currentRenderingInstance` so that this can be called in
  // a functional component
  const instance = currentInstance || currentRenderingInstance

  // also support looking up from app-level provides w/ `app.runWithContext()`
  if (instance || currentApp) {
    const provides = currentApp
      ? currentApp._context.provides
      : instance
        ? instance.parent == null
          ? instance.vnode.appContext && instance.vnode.appContext.provides
          : instance.parent.provides
        : undefined

    if (provides && key in provides) {
      return provides[key]
    } else if (arguments.length > 1) {
      return treatDefaultAsFactory && isFunction(defaultValue)
        ? defaultValue.call(instance && instance.proxy)
        : defaultValue
    } else if (__DEV__) {
      warn(`injection "${String(key)}" not found.`)
    }
  } else if (__DEV__) {
    warn(`inject() can only be used inside setup() or functional components.`)
  }
}

首先拿到當前渲染的vue例項賦值給本地變數instance。接著使用if (instance || currentApp)判斷當前是否有vue例項,如果沒有看看有沒有使用app.runWithContext手動注入了上下文,如果注入了那麼currentApp就有值。

接著就是一串三元表示式,如果使用app.runWithContext手動注入了上下文,那麼就優先從注入的上下文中取出provides屬性物件。

如果沒有那麼就看當前元件是否滿足instance.parent == null,也就是說當前元件是否是根節點。如果是根節點就取app中注入的provides屬性物件。

如果上面的都不滿足就去取父元件中注入的provides屬性物件,前面我們講過了在inject函式階段,如果子元件內沒有使用inject函式,那麼就會直接使用父元件的provides屬性物件。如果子元件中使用了inject函式,那麼就以父元件的provides屬性物件為原型去建立一個新的子元件的provides屬性物件,從而形成一條原型鏈。

所以這裡的孫子節點的provides屬性物件中當然就能夠拿到父元件中注入的count響應式變數,那麼if (provides && key in provides)就滿足條件,最後會走到return provides[key]中將父元件中注入的響應式變數count原封不動的返回。

還有就是如果我們inject一個沒有使用provide存入的key,並且傳入了第二個引數defaultValue,此時else if (arguments.length > 1)就滿足條件了。

在裡面會去判斷是否傳入第三個引數treatDefaultAsFactory,如果這個引數的值為true,說明第二個引數defaultValue可能是一個工廠函式。那麼就執行defaultValue.call(instance && instance.proxy)defaultValue的當中工廠函式的執行結果進行返回。

如果第三個引數treatDefaultAsFactory的值不為true,那麼就直接將第二個引數defaultValue當做預設值返回。

總結

這篇文章講了使用provideinject函式是如何實現資料多級傳遞的。

在建立vue元件例項時,子元件的provides屬性物件會直接使用父元件的provides屬性物件。如果在子元件中使用了provide函式,那麼會以父元件的provides屬性物件為原型建立一個新的provides屬性物件,並且將provide函式中注入的內容塞到新的provides屬性物件中,從而形成了原型鏈。

在孫子元件中,他的parent就是子元件。前面我們講過了如果沒有在元件內使用provide注入東西(很明顯這裡的子元件確實沒有注入任何東西),那麼就會直接使用他的父元件的provides屬性物件,所以這裡的子元件是直接使用的是父元件中的provides屬性物件。所以在孫子元件中可以直接使用inject函式拿到父元件中注入的內容。

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

另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。

相關文章