В Vue 3 функция setup() может быть **асинхронной**. Это позволяет использовать await прямо на верхнем уровне:
<!-- UserProfile.vue -->
<script setup>
// Компонент с async setup — await прямо на верхнем уровне
const user = await fetchUser(userId)
const posts = await fetchUserPosts(userId)
// Компонент будет ждать всех промисов перед рендером
</script>
<template>
<div>
<h1>{{ user.name }}</h1>
<PostList :posts="posts" />
</div>
</template>Компонент с async setup не будет рендериться, пока все промисы не разрешатся.
Async-компоненты **нельзя использовать без <Suspense>** родительского компонента. Иначе они просто не отобразятся. Компонент <Suspense> — это встроенный контейнер, который управляет ожиданием асинхронных дочерних компонентов:
<!-- ParentComponent.vue -->
<template>
<Suspense>
<!-- Слот #default: контент который загружается -->
<template #default>
<UserProfile :userId="42" />
</template>
<!-- Слот #fallback: что показать пока грузится -->
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
</div>
</template>
</Suspense>
</template>Хук onErrorCaptured перехватывает ошибки из дочерних компонентов, включая ошибки в async setup:
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err.message
return false // false = не распространять ошибку выше
})
</script>
<template>
<div v-if="error" class="error-banner">
Ошибка загрузки: {{ error }}
</div>
<Suspense v-else>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>Для ленивой загрузки компонентов используется defineAsyncComponent():
import { defineAsyncComponent } from 'vue'
// Простой вариант
const UserDashboard = defineAsyncComponent(
() => import('./UserDashboard.vue')
)
// Расширенный вариант с обработкой состояний
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner, // показывается пока грузится
errorComponent: ErrorDisplay, // показывается при ошибке
delay: 200, // задержка перед показом loadingComponent
timeout: 10000, // таймаут (показывает errorComponent)
})Suspense-компоненты можно вкладывать. Каждый управляет своими async-дочерними:
<Suspense>
<template #default>
<UserProfile /> <!-- своя загрузка -->
<Suspense> <!-- вложенный — независимая загрузка -->
<template #default>
<RecommendedPosts />
</template>
<template #fallback>
<PostsSkeleton />
</template>
</Suspense>
</template>
<template #fallback>
<FullPageLoader />
</template>
</Suspense>Не используй async setup для:
onMounted + реактивное состояниеЭмуляция механизма Suspense: отслеживание async-компонентов и управление fallback-состоянием
// Эмулируем поведение <Suspense>: управление async-загрузкой компонентов
// Симуляция API запросов с задержкой
function fakeApiCall(data, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`API Error: не удалось загрузить данные`))
} else {
resolve(data)
}
}, delay)
})
}
// Async-компонент: имеет async setup
class AsyncComponent {
constructor(name, setupFn) {
this.name = name
this._setupFn = setupFn
this.data = null
this.error = null
this.isReady = false
}
async setup() {
try {
this.data = await this._setupFn()
this.isReady = true
return this.data
} catch (err) {
this.error = err
throw err
}
}
render() {
if (!this.isReady) return `[${this.name}: не готов]`
return `[${this.name}: ${JSON.stringify(this.data)}]`
}
}
// Suspense-контейнер
class SuspenseContainer {
constructor(defaultSlot, fallbackSlot, onError) {
this._components = defaultSlot // async-компоненты
this._fallback = fallbackSlot
this._onError = onError
this._state = 'pending' // pending | resolved | error
}
async mount() {
console.log(` [Suspense] Начало загрузки. Показываем fallback: "${this._fallback}"`)
// Запускаем все async setup параллельно
const promises = this._components.map(comp =>
comp.setup().catch(err => {
console.log(` [Suspense] Ошибка в компоненте "${comp.name}": ${err.message}`)
if (this._onError) {
this._onError(err, comp)
}
throw err
})
)
try {
await Promise.all(promises)
this._state = 'resolved'
console.log(` [Suspense] Все компоненты загружены. Убираем fallback.`)
this._render()
} catch (err) {
this._state = 'error'
console.log(` [Suspense] Ошибка загрузки. Состояние: error`)
}
}
_render() {
console.log('\n [Suspense] Рендер default-слота:')
this._components.forEach(comp => {
console.log(' ', comp.render())
})
}
}
// === Сценарий 1: успешная загрузка ===
async function scenario1() {
console.log('=== Сценарий 1: Успешная загрузка ===')
const userComp = new AsyncComponent('UserProfile', async () => {
console.log(' [UserProfile] async setup: загружаем пользователя...')
return await fakeApiCall({ name: 'Алексей', id: 1 }, 100)
})
const postsComp = new AsyncComponent('PostList', async () => {
console.log(' [PostList] async setup: загружаем посты...')
return await fakeApiCall([{ id: 1 }, { id: 2 }], 150)
})
const suspense = new SuspenseContainer(
[userComp, postsComp],
'Skeleton-загрузчик...',
null
)
await suspense.mount()
}
// === Сценарий 2: с обработкой ошибки через onErrorCaptured ===
async function scenario2() {
console.log('\n=== Сценарий 2: Ошибка + onErrorCaptured ===')
let capturedError = null
const brokenComp = new AsyncComponent('BrokenComponent', async () => {
return await fakeApiCall(null, 100, true) // Симулируем ошибку
})
const suspense = new SuspenseContainer(
[brokenComp],
'Загрузка...',
(err, comp) => {
capturedError = err.message
console.log(` [onErrorCaptured] Перехвачена ошибка: ${err.message}`)
console.log(` [onErrorCaptured] error.value = "${err.message}" — показываем error UI`)
}
)
await suspense.mount()
console.log(` Итог: capturedError = "${capturedError}"`)
}
// Запуск
scenario1().then(() => scenario2())В Vue 3 функция setup() может быть **асинхронной**. Это позволяет использовать await прямо на верхнем уровне:
<!-- UserProfile.vue -->
<script setup>
// Компонент с async setup — await прямо на верхнем уровне
const user = await fetchUser(userId)
const posts = await fetchUserPosts(userId)
// Компонент будет ждать всех промисов перед рендером
</script>
<template>
<div>
<h1>{{ user.name }}</h1>
<PostList :posts="posts" />
</div>
</template>Компонент с async setup не будет рендериться, пока все промисы не разрешатся.
Async-компоненты **нельзя использовать без <Suspense>** родительского компонента. Иначе они просто не отобразятся. Компонент <Suspense> — это встроенный контейнер, который управляет ожиданием асинхронных дочерних компонентов:
<!-- ParentComponent.vue -->
<template>
<Suspense>
<!-- Слот #default: контент который загружается -->
<template #default>
<UserProfile :userId="42" />
</template>
<!-- Слот #fallback: что показать пока грузится -->
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-text"></div>
</div>
</template>
</Suspense>
</template>Хук onErrorCaptured перехватывает ошибки из дочерних компонентов, включая ошибки в async setup:
<script setup>
import { ref, onErrorCaptured } from 'vue'
const error = ref(null)
onErrorCaptured((err, instance, info) => {
error.value = err.message
return false // false = не распространять ошибку выше
})
</script>
<template>
<div v-if="error" class="error-banner">
Ошибка загрузки: {{ error }}
</div>
<Suspense v-else>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>Для ленивой загрузки компонентов используется defineAsyncComponent():
import { defineAsyncComponent } from 'vue'
// Простой вариант
const UserDashboard = defineAsyncComponent(
() => import('./UserDashboard.vue')
)
// Расширенный вариант с обработкой состояний
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingSpinner, // показывается пока грузится
errorComponent: ErrorDisplay, // показывается при ошибке
delay: 200, // задержка перед показом loadingComponent
timeout: 10000, // таймаут (показывает errorComponent)
})Suspense-компоненты можно вкладывать. Каждый управляет своими async-дочерними:
<Suspense>
<template #default>
<UserProfile /> <!-- своя загрузка -->
<Suspense> <!-- вложенный — независимая загрузка -->
<template #default>
<RecommendedPosts />
</template>
<template #fallback>
<PostsSkeleton />
</template>
</Suspense>
</template>
<template #fallback>
<FullPageLoader />
</template>
</Suspense>Не используй async setup для:
onMounted + реактивное состояниеЭмуляция механизма Suspense: отслеживание async-компонентов и управление fallback-состоянием
// Эмулируем поведение <Suspense>: управление async-загрузкой компонентов
// Симуляция API запросов с задержкой
function fakeApiCall(data, delay, shouldFail = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`API Error: не удалось загрузить данные`))
} else {
resolve(data)
}
}, delay)
})
}
// Async-компонент: имеет async setup
class AsyncComponent {
constructor(name, setupFn) {
this.name = name
this._setupFn = setupFn
this.data = null
this.error = null
this.isReady = false
}
async setup() {
try {
this.data = await this._setupFn()
this.isReady = true
return this.data
} catch (err) {
this.error = err
throw err
}
}
render() {
if (!this.isReady) return `[${this.name}: не готов]`
return `[${this.name}: ${JSON.stringify(this.data)}]`
}
}
// Suspense-контейнер
class SuspenseContainer {
constructor(defaultSlot, fallbackSlot, onError) {
this._components = defaultSlot // async-компоненты
this._fallback = fallbackSlot
this._onError = onError
this._state = 'pending' // pending | resolved | error
}
async mount() {
console.log(` [Suspense] Начало загрузки. Показываем fallback: "${this._fallback}"`)
// Запускаем все async setup параллельно
const promises = this._components.map(comp =>
comp.setup().catch(err => {
console.log(` [Suspense] Ошибка в компоненте "${comp.name}": ${err.message}`)
if (this._onError) {
this._onError(err, comp)
}
throw err
})
)
try {
await Promise.all(promises)
this._state = 'resolved'
console.log(` [Suspense] Все компоненты загружены. Убираем fallback.`)
this._render()
} catch (err) {
this._state = 'error'
console.log(` [Suspense] Ошибка загрузки. Состояние: error`)
}
}
_render() {
console.log('\n [Suspense] Рендер default-слота:')
this._components.forEach(comp => {
console.log(' ', comp.render())
})
}
}
// === Сценарий 1: успешная загрузка ===
async function scenario1() {
console.log('=== Сценарий 1: Успешная загрузка ===')
const userComp = new AsyncComponent('UserProfile', async () => {
console.log(' [UserProfile] async setup: загружаем пользователя...')
return await fakeApiCall({ name: 'Алексей', id: 1 }, 100)
})
const postsComp = new AsyncComponent('PostList', async () => {
console.log(' [PostList] async setup: загружаем посты...')
return await fakeApiCall([{ id: 1 }, { id: 2 }], 150)
})
const suspense = new SuspenseContainer(
[userComp, postsComp],
'Skeleton-загрузчик...',
null
)
await suspense.mount()
}
// === Сценарий 2: с обработкой ошибки через onErrorCaptured ===
async function scenario2() {
console.log('\n=== Сценарий 2: Ошибка + onErrorCaptured ===')
let capturedError = null
const brokenComp = new AsyncComponent('BrokenComponent', async () => {
return await fakeApiCall(null, 100, true) // Симулируем ошибку
})
const suspense = new SuspenseContainer(
[brokenComp],
'Загрузка...',
(err, comp) => {
capturedError = err.message
console.log(` [onErrorCaptured] Перехвачена ошибка: ${err.message}`)
console.log(` [onErrorCaptured] error.value = "${err.message}" — показываем error UI`)
}
)
await suspense.mount()
console.log(` Итог: capturedError = "${capturedError}"`)
}
// Запуск
scenario1().then(() => scenario2())Реализуй функцию `createSuspense(asyncFunctions)`, которая принимает массив async-функций, запускает их все параллельно через `Promise.all`, и возвращает объект с методом `mount()`. Метод `mount()` должен: до завершения логировать `"[fallback] Загрузка..."`, при успехе логировать `"[default] Все данные загружены"` и возвращать массив результатов, при ошибке логировать `"[error] Ошибка: <message>"` и возвращать `null`.
Используй `try/catch` вокруг `await Promise.all(asyncFunctions.map(fn => fn()))`. Перед `await` выведи fallback-сообщение. В `catch(err)` выведи ошибку через `err.message` и верни `null`.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке