依賴注入
此章節假設你已經看過了組件基礎。若你還不了解組件是什麼,請先閱讀該章節。
Prop 逐級透傳問題
通常情況下,當我們需要從父組件向子組件傳遞數據時,會使用 props。想象一下這樣的結構:有一些多層級嵌套的組件,形成了一顆巨大的組件樹,而某個深層的子組件需要一個較遠的祖先組件中的部分數據。在這種情況下,如果僅使用 props 則必須將其沿著組件鏈逐級傳遞下去,這會非常麻煩:
注意,雖然這裡的 <Footer>
組件可能根本不關心這些 props,但為了使 <DeepChild>
能訪問到它們,仍然需要定義並向下傳遞。如果組件鏈路非常長,可能會影響到更多這條路上的組件。這一問題被稱為“prop 逐級透傳”,顯然是我們希望盡量避免的情況。
provide
和 inject
可以幫助我們解決這一問題 [1]。一個父組件相對於其所有的後代組件,會作為依賴提供者。任何後代的組件樹,無論層級有多深,都可以注入由父組件提供給整條鏈路的依賴。
Provide (提供)
要為組件後代提供數據,需要使用到 provide()
函數:
vue
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
如果不使用 <script setup>
,請確保 provide()
是在 setup()
同步調用的:
js
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
provide()
函數接收兩個參數。第一個參數被稱為注入名,可以是一個字符串或是一個 Symbol
。後代組件會用注入名來查找期望注入的值。一個組件可以多次調用 provide()
,使用不同的注入名,注入不同的依賴值。
第二個參數是提供的值,值可以是任意類型,包括響應式的狀態,比如一個 ref:
js
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
提供的響應式狀態使後代組件可以由此和提供者建立響應式的聯繫。
應用層 Provide
除了在一個組件中提供依賴,我們還可以在整個應用層面提供依賴:
js
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
在應用級別提供的數據在該應用內的所有組件中都可以注入。這在你編寫插件時會特別有用,因為插件一般都不會使用組件形式來提供值。
Inject (注入)
要注入上層組件提供的數據,需使用 inject()
函數:
vue
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果提供的值是一個 ref,注入進來的會是該 ref 對象,而不會自動解包為其內部的值。這使得注入方組件能夠通過 ref 對象保持了和供給方的響應性鏈接。
同樣,如果沒有使用 <script setup>
,inject()
需要在 setup()
內同步調用:
js
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
注入默認值
默認情況下,inject
假設傳入的注入名會被某個祖先鏈上的組件提供。如果該注入名的確沒有任何組件提供,則會拋出一個運行時警告。
如果在注入一個值時不要求必須有提供者,那麼我們應該聲明一個默認值,和 props 類似:
js
// 如果沒有祖先組件提供 "message"
// `value` 會是 "這是默認值"
const value = inject('message', '這是默認值')
在一些場景中,默認值可能需要通過調用一個函數或初始化一個類來取得。為了避免在用不到默認值的情況下進行不必要的計算或產生副作用,我們可以使用工廠函數來創建默認值:
js
const value = inject('key', () => new ExpensiveClass(), true)
第三個參數表示默認值應該被當作一個工廠函數。
和響應式數據配合使用
當提供 / 注入響應式的數據時,建議盡可能將任何對響應式狀態的變更都保持在提供方組件中。這樣可以確保所提供狀態的聲明和變更操作都內聚在同一個組件內,使其更容易維護。
有的時候,我們可能需要在注入方組件中更改數據。在這種情況下,我們推薦在提供方組件內聲明並提供一個更改數據的方法函數:
vue
<!-- 在供給方組件內 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
vue
<!-- 在注入方組件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
最後,如果你想確保提供的數據不能被注入方的組件更改,你可以使用 readonly()
來包裝提供的值。
vue
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
使用 Symbol 作注入名
至此,我們已經了解瞭如何使用字符串作為注入名。但如果你正在構建大型的應用,包含非常多的依賴提供,或者你正在編寫提供給其他開發者使用的組件庫,建議最好使用 Symbol 來作為注入名以避免潛在的衝突。
我們通常推薦在一個單獨的文件中導出這些注入名 Symbol:
js
// keys.js
export const myInjectionKey = Symbol()
js
// 在供給方組件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的數據
*/ });
js
// 注入方組件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
TypeScript 用戶請參考:為 Provide / Inject 標註類型
譯者注