阿里云主机折上折
  • 微信号
Current Site:Index > The loading mechanism of asynchronous components

The loading mechanism of asynchronous components

Author:Chuan Chen 阅读数:51345人阅读 分类: Vue.js

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:

  1. Loading Phase: Executes the loader function, creating a Promise in the Pending state.
  2. Mounting Phase: If the Promise is not yet resolved, placeholder content (defaulting to null) is rendered first.
  3. 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

  1. 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()
    }
  })
})
  1. 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')
})
  1. 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:

  1. Component Level: Display error UI via errorComponent.
  2. Suspense Level: Capture errors from child components.
  3. 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:

  1. Synchronous Loading: Preload components using the __VUE_ASYNC_COMPONENTS__ identifier.
  2. Client Hydration: Match server-rendered markers to avoid reloading.
  3. 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.