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, onUnmounted } from 'vue'

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

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? Ми можемо досягти цього, також приймаючи референції як аргумент:

Для прикладу, useFetch() має мати можливість приймати референцію:

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

const { data, error } = useFetch(url)

// це має викликати повторне витягування даних
url.value = '/new-url'

Або приймати геттер:

js
// повторно отримати, коли змінюється props.id
const { data, error } = useFetch(() => `/posts/${props.id}`)

Ми можемо змінити нашу існуючу реалізацію за допомогою API watchEffect() і toValue():

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() — API, доданий у версії 3.3. Він призначений для нормалізації посилань або геттерів у значення. Якщо аргумент є посиланням, він повертає значення посилання; якщо аргумент є функцією, він її викличе та поверне її значення. В іншому випадку аргумент повертає самого себе. Він працює подібно до unref(), але з особливою обробкою функцій.

Зверніть увагу, що toValue(url) викликається всередині зворотного виклику watchEffect. Це гарантує, що спостерігач відстежує будь-які реактивні залежності, до яких звертаються під час нормалізації toValue().

Ця версія useFetch() тепер приймає статичні рядки URL-адрес, посилання та геттери, що робить її набагато гнучкішою. Ефект спостереження запуститься негайно та відстежуватиме будь-які залежності, доступ до яких здійснюється під час toValue(url). Якщо жодні залежності не відстежуються (наприклад, URL-адреса вже є рядком), ефект запускається лише один раз; інакше він запускатиметься повторно щоразу, коли відстежувана залежність зміниться.

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

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

Іменування

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

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

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

js
import { toValue } from 'vue'

function useFeature(maybeRefOrGetter) {
  // якщо maybeRefOrGetter справді є референцією або геттером,
  // буде повернено його нормалізоване значення.
  // Інакше воно буде повернуте як є
  const value = toValue(maybeRefOrGetter)
}

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

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

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

Ви, мабуть, помітили, що ми використовували виключно 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().

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

Composables слід викликати лише в <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