跳轉到內容

可組合

提示

本節假設您已具備Composition API的基本知識。如果您只使用Options API學習Vue,可以將API首選項設定為Composition API(使用左側側邊欄頂部的切換按鈕),並重新閱讀響應式基礎生命週期鉤子章節。

什麼是“可組合”?

在Vue應用中,“可組合”是一個利用Vue的Composition API來封裝和重用有狀態邏輯的函式。

在構建前端應用時,我們經常需要重用常見任務的邏輯。例如,我們可能需要在多個地方格式化日期,所以我們提取一個可重用的函式。這個格式化函式封裝了無狀態邏輯:它接受一些輸入並立即返回預期的輸出。有許多庫可以用於重用無狀態邏輯 - 例如lodashdate-fns,您可能已經聽說過。

相比之下,有狀態邏輯涉及管理隨時間變化的州。一個簡單的例子是跟蹤頁面上滑鼠的當前位置。在實際場景中,它也可能是更復雜的邏輯,如觸控手勢或與資料庫的連線狀態。

滑鼠跟蹤示例

如果我們直接在元件內部使用Composition API實現滑鼠跟蹤功能,它將看起來像這樣

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

但如果我們想在多個元件中重用相同的邏輯呢?我們可以將邏輯提取到一個外部檔案中,作為一個可組合函式

js
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// by convention, composable function names start with "use"
export function useMouse() {
  // state encapsulated and managed by the composable
  const x = ref(0)
  const y = ref(0)

  // a composable can update its managed state over time.
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // a composable can also hook into its owner component's
  // lifecycle to setup and teardown side effects.
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // expose managed state as return value
  return { x, y }
}

然後在元件中使用它這樣

vue
<script setup>
import { useMouse } from './mouse.js'

const { x, y } = useMouse()
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>
滑鼠位置在:0, 0

在沙盒中嘗試

正如我們所見,核心邏輯保持不變 - 我們所做的只是將其移動到外部函式中,並返回應該公開的狀態。就像在元件內部一樣,您可以在可組合函式中使用Composition API函式的全套功能。現在,相同的useMouse()功能可以在任何元件中使用。

儘管可組合元件有一個很好的特性,那就是你也可以巢狀它們:一個可組合函式可以呼叫一個或多個其他可組合函式。這使得我們能夠使用小的、獨立的單元來組合複雜的邏輯,就像我們使用元件來組合整個應用程式一樣。事實上,這就是我們決定將使這種模式成為可能的API集合稱為Composition API的原因。

例如,我們可以將新增和刪除DOM事件監聽器的邏輯提取到自己的可組合元件中

js
// event.js
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(target, event, callback) {
  // if you want, you can also make this
  // support selector strings as target
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

現在我們的useMouse()可組合元件可以簡化為

js
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  useEventListener(window, 'mousemove', (event) => {
    x.value = event.pageX
    y.value = event.pageY
  })

  return { x, y }
}

提示

每個呼叫useMouse()的元件例項將建立自己的xy狀態副本,這樣它們就不會相互干擾。如果你想在元件之間管理共享狀態,請閱讀狀態管理章節。

非同步狀態示例

useMouse()可組合函式不接收任何引數,那麼讓我們看看另一個使用它的例子。在進行非同步資料獲取時,我們經常需要處理不同的狀態:載入、成功和錯誤

vue
<script setup>
import { ref } from 'vue'

const data = ref(null)
const error = ref(null)

fetch('...')
  .then((res) => res.json())
  .then((json) => (data.value = json))
  .catch((err) => (error.value = err))
</script>

<template>
  <div v-if="error">Oops! Error encountered: {{ error.message }}</div>
  <div v-else-if="data">
    Data loaded:
    <pre>{{ data }}</pre>
  </div>
  <div v-else>Loading...</div>
</template>

在需要獲取資料的每個元件中重複此模式會很麻煩。讓我們將其提取到一個可組合元件中

js
// fetch.js
import { ref } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err))

  return { data, error }
}

現在在我們的元件中,我們只需要做

vue
<script setup>
import { useFetch } from './fetch.js'

const { data, error } = useFetch('...')
</script>

接受響應式狀態

useFetch()接收一個靜態URL字串作為輸入 - 因此它只執行一次獲取,然後完成。如果我們想讓它每次URL變化時都重新獲取呢?為了實現這一點,我們需要將響應式狀態傳遞給可組合函式,並讓可組合函式建立使用傳遞狀態的執行動作的監視器。

例如,useFetch()應該能夠接受一個ref

js
const url = ref('/initial-url')

const { data, error } = useFetch(url)

// this should trigger a re-fetch
url.value = '/new-url'

或者,接受一個獲取函式

js
// re-fetch when props.id changes
const { data, error } = useFetch(() => `/posts/${props.id}`)

我們可以使用watchEffect()toValue() API重構我們現有的實現

js
// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  const fetchData = () => {
    // reset state before fetching..
    data.value = null
    error.value = null

    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error }
}

toValue()是在3.3版本中新增的API。它旨在將refs或getters標準化為值。如果引數是一個ref,它返回ref的值;如果引數是一個函式,它將呼叫該函式並返回其返回值。否則,它返回引數本身。它的工作方式與unref()類似,但針對函式有特殊處理。

請注意,在watchEffect回撥中呼叫toValue(url)。這確保了在toValue()標準化過程中訪問的任何響應式依賴項都被監視器跟蹤。

這個版本的useFetch()現在接受靜態URL字串、refs和getters,使其更加靈活。監視效果將立即執行,並跟蹤在toValue(url)期間訪問的任何依賴項。如果沒有跟蹤任何依賴項(例如,url已經是一個字串),效果只執行一次;否則,它將在跟蹤的依賴項更改時重新執行。

以下是useFetch()的更新版本,用於演示目的,包含人工延遲和隨機錯誤。

約定和最佳實踐

命名

約定以camelCase命名以"use"開頭的組合函式。

輸入引數

即使組合函式不依賴於它們進行響應性,組合函式也可以接受ref或getter引數。如果您正在編寫可能被其他開發者使用的組合函式,處理輸入引數為refs或getters的情況是一個好主意。在這種情況下,toValue()實用函式會很有用。

js
import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // If maybeRefOrGetter is a ref or a getter,
  // its normalized value will be returned.
  // Otherwise, it is returned as-is.
  const value = toValue(maybeRefOrGetter)
}

如果您的組合函式在輸入為ref或getter時建立響應式效果,請確保使用watch()顯式地監視ref/getter,或者在一個watchEffect()呼叫中呼叫toValue(),以確保它得到適當的跟蹤。

前面討論的useFetch()實現提供了接受refs、getters和普通值作為輸入引數的組合函式的具體示例。

返回值

您可能已經注意到,我們在組合函式中一直使用ref()而不是reactive()。建議的組合函式約定是始終返回一個包含多個refs的普通、非響應式物件。這使得它可以被元件解構,同時保持響應性。

js
// x and y are refs
const { x, y } = useMouse()

從組合函式返回響應式物件將導致這些解構失去與組合函式內部狀態的響應性連線,而refs將保持這種連線。

如果您更喜歡將組合函式返回的狀態作為物件屬性使用,可以使用reactive()包裝返回的物件,以便取消refs的包裝。例如

js
const mouse = reactive(useMouse())
// mouse.x is linked to original ref
console.log(mouse.x)
模板
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}

副作用

在組合函式中執行副作用(例如新增DOM事件監聽器或獲取資料)是可以的,但請注意以下規則。

  • 如果您正在開發一個使用伺服器端渲染(SSR)的應用程式,請確保在生命週期鉤子中執行DOM特定的副作用,例如onMounted()。這些鉤子僅在瀏覽器中呼叫,因此您可以確定它們內部的程式碼可以訪問DOM。

  • 請記住在 onUnmounted() 中清理副作用。例如,如果組合式元件設定了 DOM 事件監聽器,它應該在 onUnmounted() 中移除該監聽器,正如我們在 useMouse() 示例中所見。使用自動為您完成此操作的組合式,如 useEventListener() 示例,可能是一個好主意。

使用限制

組合式只能在 <script setup>setup() 鉤子中呼叫。它們也應該在這些上下文中以 同步 的方式呼叫。在某些情況下,您還可以在生命週期鉤子如 onMounted() 中呼叫它們。

這些限制很重要,因為這些是 Vue 能夠確定當前活動元件例項的上下文。訪問活動元件例項是必要的,以便

  1. 將生命週期鉤子註冊到它。

  2. 計算屬性和偵聽器可以與之關聯,這樣它們就可以在例項解除安裝時被銷燬,以防止記憶體洩漏。

提示

<script setup> 是唯一可以呼叫組合式並在使用 await 後進行呼叫的地方。編譯器會在非同步操作後自動為您恢復活動例項上下文。

提取組合式以進行程式碼組織

組合式不僅可以用於複用,還可以用於程式碼組織。隨著元件複雜性的增加,您可能會得到難以導航和推理的元件。組合式 API 可以讓您完全靈活地將元件程式碼組織成基於邏輯關注點的小函式

vue
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'

const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>

在某種程度上,您可以把這些提取的組合式看作是元件範圍內的服務,它們可以相互通訊。

在 Options API 中使用組合式

如果您正在使用 Options API,組合式必須在 setup() 中呼叫,並且必須從 setup() 中返回返回的繫結,以便它們暴露給 this 和模板

js
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'

export default {
  setup() {
    const { x, y } = useMouse()
    const { data, error } = useFetch('...')
    return { x, y, data, error }
  },
  mounted() {
    // setup() exposed properties can be accessed on `this`
    console.log(this.x)
  }
  // ...other options
}

與其他技術的比較

與 Mixins 的比較

來自 Vue 2 的使用者可能熟悉 mixins 選項,它也允許我們將元件邏輯提取成可重用的單元。mixins 有三個主要的缺點

  1. 屬性來源不明確:當使用許多 mixins 時,變得不清楚哪個例項屬性是由哪個 mixin 注入的,這使得跟蹤實現和理解元件的行為變得困難。這也是我們為什麼建議使用 refs + destructure 模式來建立組合式:它在消費元件中使屬性來源更清晰。

  2. 名稱空間衝突:來自不同作者的多 mixins 可能會註冊相同的屬性鍵,導致名稱空間衝突。使用組合式時,如果不同組合式有衝突的鍵,可以重新命名解構變數。

  3. 隱式跨 mixin 通訊:需要相互互動的多個 mixins 必須依賴於共享的屬性鍵,這使得它們隱式耦合。使用組合式時,可以從一個組合式傳遞值到另一個作為引數,就像正常函式一樣。

出於上述原因,我們不再建議在 Vue 3 中使用 mixins。該功能僅保留用於遷移和熟悉性原因。

與無狀態元件相比

在元件槽位章節中,我們討論了基於作用域槽位的 無狀態元件 模式。我們還使用無狀態元件實現了相同的滑鼠跟蹤演示。

可組合元件相較於無狀態元件的主要優勢是,可組合元件不會產生額外的元件例項開銷。當在整個應用程式中使用時,無狀態元件模式建立的額外元件例項數量可能會成為明顯的效能開銷。

建議在重用純邏輯時使用可組合元件,在重用邏輯和視覺佈局時使用元件。

與 React Hooks 相比

如果您有 React 的經驗,您可能會注意到這看起來非常類似於自定義 React Hooks。組合 API 在一定程度上受到了 React Hooks 的啟發,Vue 的可組合元件確實在邏輯組合能力方面與 React Hooks 類似。然而,Vue 的可組合元件是基於 Vue 的細粒度響應式系統,這與 React Hooks 的執行模型在本質上不同。這將在組合 API FAQ中更詳細地討論。

進一步閱讀

  • 深入響應式:瞭解 Vue 的響應式系統是如何工作的。
  • 狀態管理:瞭解管理多個元件共享狀態的模式。
  • 測試可組合元件:單元測試可組合元件的技巧。
  • VueUse:一個不斷增長的 Vue 可組合元件集合。原始碼也是一個很好的學習資源。
已載入可組合元件