Vue 3 под капотом и тонкости Composition API: Reactivity, Provide/Inject, Suspense

Изображение, созданное DALL-E

Изображение, созданное DALL-E

Vue 3 не только добавил новый синтаксис (Composition API), но и серьёзно обновил движок реактивности. Теперь под капотом используются прокси-объекты (ES6 Proxy), а при отслеживании и изменении данных происходят события Track и Trigger. Эти детали могут быть неочевидны в простых демо-примерах, но становятся крайне важными, когда вы работаете с большими структурами данных или строите действительно масштабные приложения.

С обновлениями до версии 3.5 улучшения стали ещё более заметными: Vue научился лучше обрабатывать глубоко вложенные структуры данных, оптимизировал производительность реактивности и усилил поддержку асинхронных процессов.

В этой статье разберём:

  1. Реактивность в глубину: как Vue 3 следит за изменениями, что такое Track/Trigger, как оптимизировать работу с вложенными объектами, и какие инструменты для отладки могут помочь.

  2. Сложные сценарии с provide/inject и customRef: когда эти механизмы полезны, как управлять глубокой иерархией компонентов, и как customRef решает задачи с debounce.

  3. Suspense и асинхронные данные: что такое , как работает async setup(), какие преимущества даёт при работе с динамическими компонентами и загрузкой больших данных, а также как обрабатывать ошибки с помощью Error Boundaries.

Поехали!

Реактивность в глубину: Proxy, Track, Trigger

Как Vue 3 следит за изменениями

В Vue 2 механизм реактивности строился на Object.defineProperty, который перехватывал геттеры/сеттеры каждого свойства. Этот подход имел ограничения, например, не отслеживал динамически добавленные свойства и был менее гибким при работе со вложенными структурами.

В Vue 3 вместо этого используется Proxy. Когда вы создаёте реактивные данные (через reactive() или ref()), Vue оборачивает исходный объект в прокси, чтобы ловить все операции чтения/записи:

  • track срабатывает, когда мы читаем свойство (геттер).

  • trigger срабатывает, когда мы записываем (сеттер).

С обновлениями версии 3.5 Vue улучшил работу track и trigger для сложных структур, оптимизировав их производительность. Теперь изменения в глубоко вложенных объектах инициализируются только при необходимости, что снижает накладные расходы.

Простой пример с effect() (в реальном приложении Vue сама под капотом использует рендер-эффекты):

import { reactive, effect } from 'vue'

const state = reactive({ count: 0 })

effect(() => {
  console.log(`Count is: ${state.count}`)
})

// При изменении state.count (trigger) автоматически вызывается effect
state.count++
// console.log выведет: "Count is: 1"

В реальном приложении вместо effect() Vue под капотом использует собственные эффекты рендера, чтобы обновлять шаблон или virtual DOM.

Track/Trigger на практике

Под капотом всё выглядит так (упрощённо):

  • track(target, type, key) — Подпишись на изменения свойства key у объекта target, если при рендере мы читали это свойство.

  • trigger(target, type, key, newValue, oldValue) — Уведоми всех подписчиков, когда свойство key у объекта target меняется.

Если у вас есть глубокие вложенные структуры (например, state.user.profile.address), Vue 3 создаст прокси для каждого уровня, чтобы при доступе к address.city происходил track, а при изменении city — trigger.

Оптимизация при работе с большими объектами

Для работы с большими структурами данных в Vue 3.5 появились дополнительные оптимизации, которые стоит учитывать:

  • Ленивая инициализация: Прокси для вложенных объектов создаются только при обращении к этим объектам. Это уменьшает нагрузку на память и улучшает производительность.

  • Использовать shallowReactive и shallowRef.Эти функции поверхностно отслеживают только верхний уровень объекта и не спускаются глубже. Если вам нужно реактивно заменить весь объект целиком, но не важно, что происходит внутри, это может быть отличным решением.

  • Делить объект на логические модули. Вместо одного огромного store разбивайте данные на более мелкие подхранилища. В экосистеме Vue 3 для этого отлично подойдёт Pinia или несколько отдельных composable-функций.

Инструменты для отладки: Vue Devtools 6+

Когда вы работаете со сложной реактивностью, полезно следить, как Vue отслеживает изменения. В версии Vue Devtools 6 (и выше) есть расширенный вклад Timeline, где можно увидеть события component render, update, и другие — это упрощает понимание, какой именно фрагмент кода или объект триггерит повторный рендер. Также существуют экспериментальные плагины, показывающие track/trigger в реальном времени, но они могут меняться от релиза к релизу.

Сложные сценарии с provide/inject и customRef

provide/inject для больших деревьев

provide и inject позволяют протягивать данные через несколько уровней компонентов без явной передачи пропсов. Это особенно актуально, когда у вас глобальные данные или сервисы (например, тема приложения, текущий пользователь, WebSocket-соединение).

Упрощённый пример:










Здесь нет необходимости пробрасывать theme через каждый уровень компонентов в виде пропсов. Однако, когда в коде много таких глобальных переменных (цвета, настройки, текущий пользователь и т.д.), будьте аккуратны с выбором ключей (provide ('themeState', …) и т.п.). Если вы используете TypeScript, применяйте символы (Symbol) вместо строк, чтобы избежать коллизий. Suspense и асинхронные данные

Проблемы производительности

Если у вас сложная логика, слушающая изменения в themeState (или другом глобальном объекте), и это приводит к каскадному ререндеру многих компонентов, подумайте о разделении:

  • Использовать несколько provide для разных сущностей (цвет, шрифт, лейаут).

  • Применять shallowReactive, если нужно менять объект целиком, а не отдельные поля.

  • Организовывать обновление только там, где оно действительно нужно, а в остальных местах (детях) использовать мемоизацию (в Composition API это может быть computed() или закешированные значения).

customRef для тонкой настройки рендера

Иногда нужно полностью контролировать момент, когда произойдёт перерисовка. customRef даёт возможность вручную описать логику при чтении (get) и записи (set). Типичный пример — debounce.

import { customRef } from 'vue'

function useDebouncedRef(value, delay = 500) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // подпишемся на чтение
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // уведомим, что значение изменилось
        }, delay)
      }
    }
  })
}

export default useDebouncedRef

Как использовать:



Теперь при вводе в , ререндер срабатывает не на каждый символ, а только через 500 мс тишины. Это заметно улучшает производительность, если, скажем, мы вызываем тяжелый запрос по мере ввода.

Suspense и асинхронные данные

Что такое?

 — это специальный компонент в Vue 3, который позволяет показывать заглушку (fallback), пока внутри него происходит асинхронная загрузка данных. Идея пришла из React (React Suspense), но Vue адаптировал её под свою модель.

Пример:



Пока AsyncDataComponent не загрузит данные (например, делает запрос на сервер в setup()), будет отображать блок