The loading mechanism of asynchronous components
Asynchronous Component Loading Mechanism
Vue 3's asynchronous component loading mechanism achieves code splitting through dynamic imports, significantly improving application performance. The core logic revolves around defineAsyncComponent
, handling loading states and error boundaries while supporting advanced configurations such as timeout control and lazy loading.
Basic Implementation Principle
The essence of asynchronous components is a factory function that returns a Promise. When the component is first rendered, it triggers the loading process, and upon success, the result is cached for subsequent use. The basic declaration is as follows:
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
The underlying process consists of three stages:
- Loading Phase: Executes the loader function, creating a Promise in the Pending state.
- Mounting Phase: If the Promise is not yet resolved, placeholder content (defaulting to null) is rendered first.
- Completion Phase: Upon successful resolution, the actual component is rendered; if it fails, the error component is displayed.
Advanced Configuration Parameters
defineAsyncComponent
accepts a configuration object for fine-grained control:
const AsyncWithOptions = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
delay: 200, // Delay in milliseconds before showing loading state
timeout: 3000, // Timeout duration (default: Infinity)
loadingComponent: LoadingSpinner, // Loading component
errorComponent: ErrorDisplay, // Error component
suspensible: false // Whether to be controlled by Suspense
})
Typical scenario handling logic:
- Fast Loading (<200ms): No loading state is displayed.
- Medium Loading Time (200-3000ms): The
loadingComponent
is displayed. - Timeout Scenario (>3000ms): The
errorComponent
is displayed.
Collaboration with Suspense
When configured with suspensible: true
, the asynchronous component will look up to the nearest <Suspense>
as its boundary. In this case, the loading state is managed by Suspense, forming a nested asynchronous flow:
<Suspense>
<template #default>
<AsyncDashboard />
</template>
<template #fallback>
<GlobalLoading />
</template>
</Suspense>
Nested asynchronous components trigger Suspense's waterfall loading effect, where the fallback content is replaced only after all child components have finished loading.
Underlying Source Code Analysis
The key implementation is located in runtime-core/src/apiAsyncComponent.ts
:
function defineAsyncComponent(options) {
if (isFunction(options)) {
options = { loader: options }
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout,
suspensible = true
} = options
let pendingRequest = null
return defineComponent({
__asyncLoader: load,
setup() {
const loaded = ref(false)
const error = ref(null)
const delayed = ref(!!delay)
if (delayed.value) {
setTimeout(() => {
delayed.value = false
}, delay)
}
// Loading logic
const load = () => {
pendingRequest = loader()
.then(comp => {
loaded.value = true
return comp
})
.catch(err => {
error.value = err
throw err
})
return pendingRequest
}
// Handle timeout
if (timeout != null) {
setTimeout(() => {
if (!loaded.value && !error.value) {
error.value = new Error(`Async component timed out after ${timeout}ms`)
}
}, timeout)
}
return () => {
if (loaded.value) {
return h(resolvedComp)
} else if (error.value && errorComponent) {
return h(errorComponent, { error: error.value })
} else if (delayed.value) {
return null
} else {
return loadingComponent ? h(loadingComponent) : null
}
}
}
})
}
Performance Optimization Practices
- Preloading Strategy: Preload components using the router's
beforeResolve
hook:
router.beforeResolve((to) => {
const components = router.resolve(to).matched.flatMap(record =>
Object.values(record.components)
)
components.forEach(comp => {
if (typeof comp === 'function' && comp.__asyncLoader) {
comp.__asyncLoader()
}
})
})
- Group Loading: Bundle related components into the same chunk:
const UserProfile = defineAsyncComponent({
loader: () => import(/* webpackChunkName: "user" */ './UserProfile.vue')
})
const UserSettings = defineAsyncComponent({
loader: () => import(/* webpackChunkName: "user" */ './UserSettings.vue')
})
- Priority Loading: Use
webpackPrefetch
for critical components:
const PaymentForm = defineAsyncComponent({
loader: () => import(/* webpackPrefetch: true */ './PaymentForm.vue')
})
Error Handling Mechanism
The complete error handling chain includes three levels:
- Component Level: Display error UI via
errorComponent
. - Suspense Level: Capture errors from child components.
- Global Level: Handle errors uniformly with
app.config.errorHandler
.
Special error type handling:
const AsyncWithRetry = defineAsyncComponent({
loader: () => import('./UnstableComponent.vue'),
errorComponent: {
setup(props, { emit }) {
const handleRetry = () => emit('retry')
return () => h('button', { onClick: handleRetry }, 'Retry')
}
},
onError(error, retry, fail) {
if (error.message.includes('NetworkError')) {
retry()
} else {
fail()
}
}
})
Server-Side Rendering Adaptation
Special handling is required for SSR environments:
- Synchronous Loading: Preload components using the
__VUE_ASYNC_COMPONENTS__
identifier. - Client Hydration: Match server-rendered markers to avoid reloading.
- Loading State Serialization: Inline resolved components directly into HTML.
Example SSR configuration:
const AsyncSSRComponent = defineAsyncComponent({
loader: () => import('./SSRComponent.vue'),
serverLoader: async () => {
// Server-specific loading logic
return (await import('./SSRComponent.vue')).default
}
})
Deep Integration with Dynamic Routing
Best practices when integrating with Vue Router:
const router = createRouter({
routes: [{
path: '/user/:id',
component: () => import('./UserDetails.vue'),
props: route => ({ id: route.params.id })
}]
})
// Route-level code splitting
function lazyLoad(view) {
return defineAsyncComponent({
loader: () => import(`../views/${view}.vue`),
loadingComponent: RouteLoading,
delay: 100
})
}
Rendering Behavior Details
The rendering timing of asynchronous components follows Vue's reactivity rules:
- Loading is triggered during initial rendering.
- Maintains reactive state: If the component is unmounted and remounted, the cached result is used if the Promise is already resolved.
- Context inheritance: Correct provide/inject context is accessible even during loading.
Special scenario example:
const Parent = defineComponent({
provide: { theme: 'dark' },
setup() {
const showChild = ref(false)
return { showChild }
},
template: `
<button @click="showChild = true">Load</button>
<AsyncChild v-if="showChild" />
`
})
const AsyncChild = defineAsyncComponent({
loader: () => import('./Child.vue'),
setup() {
const theme = inject('theme') // Correctly retrieves 'dark'
return { theme }
}
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:模板引用的实现方式
下一篇:自定义渲染逻辑的实现