Skip to content

Композиційні функції

TIP

Цей розділ передбачає базові знання композиційного API. Якщо ви вивчали Vue лише з опційним API, ви можете встановити налаштування API на композиційний (за допомогою перемикача у верхній частині лівої бічної панелі) і перечитати основи реактивності та розділи по хуках життєвого циклу.

Що таке композиційна функція?

У контексті додатків Vue композиційна функція — це функція, яка використовує композиційний API Vue для інкапсуляції та повторного використання логіки зі станом.

Під час створення фронтенд додатків нам часто потрібно повторно використовувати логіку для типових завдань. Наприклад, нам може знадобитися форматування дати в багатьох місцях, тому ми беремо для цього функцію для повторного використання. Ця функція форматування інкапсулює логіку без стану: вона приймає деякий вхід і негайно повертає очікуваний результат. Існує багато бібліотек для повторного використання логіки без стану, наприклад lodash і date-fns, про які ви, можливо, чули.

Навпаки, логіка збереження стану передбачає керування станом, який змінюється з часом. Простим прикладом може бути відстеження поточної позиції миші на сторінці. У реальних сценаріях це також може бути складніша логіка, наприклад жести дотиком або статус підключення до бази даних.

Приклад відстеження миші

Якби ми реалізували функцію відстеження миші за допомогою композиційного 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>Координати миші: {{ x }}, {{ y }}</template>

Але що, якщо ми хочемо повторно використовувати ту саму логіку в кількох компонентах? Ми можемо перемістити логіку у зовнішній файл як композиційну функцію:

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

// за конвенцією, назви композиційних функцій починаються з "use" (англ. — використовувати)
export function useMouse() {
  // стан, інкапсульований і керований композиційною функцією
  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))

  // відкрити керований стан як значення, що повертається
  return { x, y }
}

І ось як це можна використовувати в компонентах:

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

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

<template>Координати миші: {{ x }}, {{ y }}</template>
Координати миші: 0, 0

Спробуйте в пісочниці

Як ми бачимо, основна логіка залишається ідентичною - все, що нам потрібно було зробити, це перемістити її в зовнішню функцію і повернути стан, який повинен бути відкритий. Так само як і всередині компонента, ви можете використовувати повний діапазон функцій композиційного API у композиційних функціях. Ту саму функцію useMouse() тепер можна використовувати в будь-якому компоненті.

Але крутіша частина композиційних функцій полягає в тому, що ви також можете вкладати їх: одна композиційна функція може викликати одну або кілька інших композиційних функцій. Це дає нам змогу створювати складну логіку за допомогою невеликих ізольованих одиниць, подібно до того, як ми створюємо цілу програму за допомогою компонентів. Ось чому ми вирішили назвати набір API, які роблять можливим цей шаблон композиційним API.

Наприклад, ми можемо витягти логіку додавання та видалення слухача подій DOM у свою власну композиційну функцію:

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

export function useEventListener(target, event, callback) {
  // за бажанням, ви також можете додати 
  // підтримку рядків в якості цілі для прослуховування
  onMounted(() => target.addEventListener(event, callback))
  onBeforeUnmount(() => 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 }
}

TIP

Кожен екземпляр компонента, що викликає useMouse(), створить власні копії стану x і y, щоб вони не заважали один одному. Якщо ви хочете керувати спільним станом між компонентами, прочитайте розділ Керування станом.

Приклад асинхронного стану

Композиційна функція 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">Ой! Виникла помилка: {{ error.message }}</div>
  <div v-else-if="data">
    Дані завантажено:
    <pre>{{ data }}</pre>
  </div>
  <div v-else>Завантаження...</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? Ми можемо досягти цього, також приймаючи референції як аргумент:

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

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

  function doFetch() {
    // скидання стану перед отриманням..
    data.value = null
    error.value = null
    // unref() розгортає потенційні референції
    fetch(unref(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  }

  if (isRef(url)) {
    // налаштувати реактивне повторне витягнення даних, якщо вхідна URL-адреса є референцією
    watchEffect(doFetch)
  } else {
    // інакше просто витягуємо один раз,
    // уникаючи накладних витрат спостерігача
    doFetch()
  }

  return { data, error }
}

Ця версія useFetch() тепер приймає як статичні рядки URL-адреси, так і референції на рядки URL-адреси. Коли він виявляє, що URL-адреса є динамічною референцією за допомогою isRef(), він встановлює реактивний ефект за допомогою watchEffect(). Ефект запуститься негайно, а також відстежуватиме URL-адресу як залежність. Щоразу, коли URL-адреса змінюється, дані скидаються та завантажуються знову.

Ось оновлена версія useFetch(), зі штучною затримкою та випадковою помилкою для демонстраційних цілей.

Конвенції та найкращі практики

Іменування

Визначає правило іменування композиційних функцій за допомогою імен верблюжого регістру, які починаються з "use".

Вхідні аргументи

Композиційна функція може приймати референції як аргументи, навіть якщо вона не покладається на них для реактивності. Якщо ви пишете композиційну функцію, яка може використовуватися іншими розробниками, буде гарною ідеєю розглянути випадок, коли вхідні аргументи є референціями замість необроблених значень. Допоміжна функція unref() стане в пригоді для цієї мети:

js
import { unref } from 'vue'

function useFeature(maybeRef) {
  // якщо maybeRef справді є референцією, буде повернено його .value
  // інакше maybeRef повертається як є
  const value = unref(maybeRef)
}

Якщо ваша композиційна функція створює реактивні ефекти, коли вхідний аргумент є референцією, переконайтеся, що ви явно спостерігаєте за посиланням за допомогою watch(), або викликаєте unref() всередині watchEffect(), щоб воно належним чином відстежувалося.

Повернуті значення

Ви, мабуть, помітили, що ми використовували виключно ref() замість reactive() у композиційних функціях. Згідно з конвенцією, рекомендується, щоб композиційні функції завжди повертали звичайний нереактивний об’єкт, що містить кілька референцій. Це дозволяє його деструктурувати на компоненти, зберігаючи реакційну здатність:

js
// x і y є референціями
const { x, y } = useMouse()

Повернення реактивного об'єкта з композиційної функції призведе до того, що такі деструктуризовані дані втратять зв'язок реактивності зі станом всередині композиційної функції, тоді як референції збережуть цей зв'язок.

Якщо ви віддаєте перевагу використанню повернутого стану від композиційної функції як властивості об'єкта, ви можете обгорнути повернутий об'єкт за допомогою reactive(), щоб референції були розгорнутими. Наприклад:

js
const mouse = reactive(useMouse())
// mouse.x пов'язано з оригінальною референцією
console.log(mouse.x)
template
Координати миші: {{ mouse.x }}, {{ mouse.y }}

Сторонні ефекти

Виконувати побічні ефекти (наприклад, додавати прослуховувачі подій DOM або отримувати дані) у композиційних функціях можна, але зверніть увагу на наступні правила:

  • Якщо ви працюєте над програмою, яка використовує рендеринг на стороні сервера (SSR), переконайтеся, що ви виконуєте специфічні для DOM побічні ефекти в хуках життєвого циклу після монтування, наприклад, onMounted(). Ці хуки викликаються лише в браузері, тож ви можете бути впевнені, що код у них має доступ до DOM.

  • Не забудьте очищувати побічні ефекти в onUnmounted(). Наприклад, якщо композиційна функція встановлює слухач подій DOM, він повинен видалити цей слухач у onUnmounted(), як ми бачили у прикладі useMouse(). Гарною ідеєю може бути використання композиційної функції, яка автоматично робить це за вас, як-от приклад useEventListener().

Обмеження при використанні

Композиційні функції слід викликати лише синхронно в <script setup> або setup() хуку. У деяких випадках ви також можете викликати їх у хуках життєвого циклу, наприклад onMounted().

Це контексти, у яких Vue може визначити поточний екземпляр активного компонента. Доступ до екземпляра активного компонента необхідний для того, щоб:

  1. В ньому можна зареєструвати хуки життєвого циклу.

  2. До нього можна прив'язати обчислювані властивості та спостерігачі, щоб їх можна було видалити, коли екземпляр відмонтовано, щоб запобігти джерелам витоку пам'яті.

TIP

<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>

Певною мірою ви можете розглядати ці витягнуті компоненти як компонентні сервіси, які можуть спілкуватися один з одним.

Використання композиційних функцій в опційному API

Якщо ви використовуєте опційний 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() можна отримати через `this`
    console.log(this.x)
  }
  // ...інші варіанти
}

Порівняння щодо інших технік

щодо міксинів

Користувачі, які перейшли з Vue 2, можуть бути знайомі з параметром mixins, який також дозволяє нам витягувати логіку компонентів у багаторазові блоки. У міксинів є три основні недоліки:

  1. Незрозуміле джерело властивостей: при використанні багатьох міксинів стає незрозуміло, яка властивість екземпляра впроваджується яким міксином, що ускладнює відстеження реалізації та розуміння поведінки компонента. Ось чому ми також рекомендуємо використовувати шаблон "референція + деструктуризація" для композиційних функцій: це робить джерело властивості зрозумілим у споживаючих компонентах.

  2. Колізії просторів імен: кілька міксинів від різних авторів потенційно можуть зареєструвати однакові ключі властивостей, спричиняючи колізії просторів імен. За допомогою композиційниї функцій ви можете перейменувати деструктуровані змінні, якщо є конфліктні ключі від різних композиційних функцій.

  3. Неявний зв'язок крос-міксинів: кілька міксинів, які повинні взаємодіяти один з одним, повинні покладатися на спільні ключі властивостей, що робить їх неявно зв'язаними. За допомогою композиційних функцій значення, повернуті однією композиційною функцією, можна передати в інший як аргументи, як і у звичайні функції.

З наведених вище причин ми більше не рекомендуємо використовувати міксини у Vue 3. Ця функція зберігається лише з міркувань міграції та знайомства.

щодо компонентів без рендерингу

У розділі про слоти компонентів ми обговорили шаблон компоненти без рендеру на основі слотів з обмеженою областю. Ми навіть реалізували ту саму демонстрацію відстеження миші, використовуючи компоненти без рендерингу.

Основна перевага складових компонентів над компонентами без рендерингу полягає в тому, що складові компоненти не спричиняють додаткових витрат на екземпляр компонента. При використанні в усій програмі, кількість додаткових екземплярів компонентів, створених за допомогою шаблону компонента без рендерингу, може призвести до помітних накладних витрат на продуктивність.

Рекомендується використовувати композиційні функції при повторному використанні чистої логіки та використовувати компоненти при повторному використанні як логіки, так і візуального макета.

щодо React хуків

Якщо у вас є досвід роботи з React, ви можете помітити, що це дуже схоже на спеціальні хуки React. Композиційний API був частково натхненний хуками React, і композиційні функції Vue справді схожі на хуки React з точки зору можливостей логічної композиції. Однак, композиційні функції Vue базуються на багатогранній системі реактивності Vue, яка принципово відрізняється від моделі виконання хуків React. Це обговорюється більш детально в поширених питаннях щодо композиційного API.

Подальше читання

Композиційні функції has loaded