跳转到内容

Vue 异步组件实现原理

刀刀
0字
0分钟
2026/8/16

Vue中,defineAsyncComponent 方法用于定义异步组件,实现按需加载,减少初始加载时间,提高性能。

传入一个返回 Promise 的函数,Promise 中返回组件对象即可异步加载。

vue
<script setup>
import { defineAsyncComponent, h } from 'vue'
const AsyncComponent = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    const comp = {
      setup() {
        // setup 返回的这个函数就是render函数
        // render 函数返回的就是 vNode
        return () => h('div', '异步组件')
      },
    }
    setTimeout(() => {
      resolve(comp)
    }, 2000)
  })
})
</script>

初步实现

占位符渲染

实现一个简单的 defineAsyncComponent,不考虑传参,返回占位符并渲染在页面上。

vue
<script setup lang="ts">
import { defineAsyncComponent } from './index'

const AsyncComponent = defineAsyncComponent()
</script>

<template>
  <AsyncComponent />
</template>

<style scoped></style>
ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = () => {
  return {
    setup() {
      // 这是一个函数式组件
      const component = shallowRef(() => h('div', '占位符'))
      return () => {
        return h(component.value)
      }
    },
  }
}

异步函数加载

接收一个参数,类型为函数,返回 PromisePromise 中返回组件对象。

defineAsyncComponent 中调用该函数,函数执行完毕后在 .then 中将返回的组件对象赋值给 component.value,实现异步加载。

vue
<script setup lang="ts">
import { h } from 'vue'
import { defineAsyncComponent } from './index'
const AsyncComponent = defineAsyncComponent(() => {
  const comp = () => h('div', 'hello world') 
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(comp) 
    }, 2000) 
  }) 
})
</script>

<template>
  <AsyncComponent />
</template>

<style scoped></style>
ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = (loader) => {
  return {
    setup() {
      // 这是一个函数式组件
      const component = shallowRef(() => h('div', ''))
      // 执行回调函数,获取组件
      loader().then((res) => {
        component.value = res 
      }) 
      return () => {
        return h(component.value)
      }
    },
  }
}

去掉占位符文本,刷新查看效果:页面一开始为空,2s 后出现 hello world

需求增加

传参适配

增加需求:不仅能传递函数,还能传递对象,对象包含两个属性:

  • loader:返回 Promise 的函数,Promise 中返回组件对象。
  • loadingComponent:加载中组件,异步加载过程中渲染。

defineAsyncComponent 中判断参数类型:如果是函数,转换为对象,将回调函数赋值给 loader 属性;如果是对象,直接使用。

后续使用只需解构 loaderloadingComponent,无需关心传参类型。

vue
<script setup lang="ts">
import { h } from 'vue'
import { defineAsyncComponent } from './index'

const AsyncComponent = defineAsyncComponent({
  loader: () => {
    const comp = () => h('div', 'hello world') 
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(comp) 
      }, 2000) 
    }) 
  }, 
  loadingComponent: () => h('div', 'loading...'), 
}) 
</script>

<template>
  <AsyncComponent />
</template>

<style scoped></style>
ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = (options) => {
  // 不再考虑options的类型做不同的使用,如果它是函数,那么就转换为一个对象
  if (typeof options === 'function') {
    options = { loader: options } 
  } 
  const defineComponent = () => h('div', '') // 默认占位组件

  const { loader, loadingComponent = defineComponent } = options 

  return {
    setup() {
      // 这是一个函数式组件
      const component = shallowRef(loadingComponent) 
      // 执行回调函数,获取组件
      loader().then((res) => {
        component.value = res
      })
      return () => {
        return h(component.value)
      }
    },
  }
}

还能自定义默认占位符组件,用户未传 loadingComponent 时渲染默认占位符。

错误处理

当前只处理了加载成功的情况。如果加载失败,页面一直停留在 loadingComponent 状态。需要处理加载失败。

传参对象中新增 errorComponent 属性,用于加载失败。defineAsyncComponent 中加载失败时将 errorComponent 赋值给 component.value

loadingComponent 一样,未传 errorComponent 时渲染默认占位符。

vue
<script setup lang="ts">
import { h } from 'vue'
import { defineAsyncComponent } from './index'

const AsyncComponent = defineAsyncComponent({
  loader: () => {
    const comp = () => h('div', 'hello world')
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(comp) 
      }, 2000)
    })
  },
  loadingComponent: () => h('div', 'loading...'),
  errorComponent: () => h('div', 'load error, please retry'), 
})
</script>

<template>
  <AsyncComponent />
</template>

<style scoped></style>
ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = (options) => {
  // 不再考虑options的类型做不同的使用,如果它是函数,那么就转换为一个对象
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const defineComponent = () => h('div', '') // 默认占位组件

  const {
    loader,
    loadingComponent = defineComponent,
    errorComponent = defineComponent,
  } = options 

  return {
    setup() {
      // 这是一个函数式组件
      const component = shallowRef(loadingComponent)
      // 执行回调函数,获取组件
      loader()
        .then((res) => {
          component.value = res
        })
        .catch(() => {
          component.value = errorComponent 
        }) 
      return () => {
        return h(component.value)
      }
    },
  }
}

超时处理

组件加载时间过长(如 10s),页面一直停留在 loadingComponent 状态。需要处理超时。

传参对象中新增 timeout 属性。在 defineAsyncComponent 中加载超时后将 errorComponent 赋值给 component.value

直接在外部写 setTimeout 不可行。假设超时 3s,Promise 在 4s 执行成功,则无法因超时修改 Promise 状态,导致组件从 errorComponent 变为 loader

回顾当前代码,defineAsyncComponent 本质是根据不同状态渲染不同组件:pendingloadingComponentfulfilledloader 返回的组件;rejectederrorComponent

可以封装一个函数返回 Promise,在 loader 执行成功后变为 fulfilled,失败后变为 rejected。这样就能在超时后手动改为 rejected

ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = (options) => {
  // 不再考虑options的类型做不同的使用,如果它是函数,那么就转换为一个对象
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const defineComponent = () => h('div', '') // 默认占位组件

  const {
    loader,
    loadingComponent = defineComponent,
    errorComponent = defineComponent,
    timeout = 1000,
  } = options

  // 再写一个函数,返回一个new Promise。这样就能手动修改new Promise的状态
  const loaderComponent = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('加载超时') 
      }, timeout) 
      loader().then(resolve, reject) 
    }) 
  } 

  return {
    setup() {
      // 这是一个函数式组件
      const component = shallowRef(loadingComponent)
      // 执行回调函数,获取组件
      loaderComponent() 
        .then((res) => {
          component.value = res
        })
        .catch(() => {
          component.value = errorComponent
        })
      return () => {
        return h(component.value)
      }
    },
  }
}

import、props 与 slot

一般用户不会写函数作为 loader,而是写 import 语句引入组件。因此需要支持 import 语句。

如何判断用户传的是 import 语句?

import导入的打印

打印发现它有一个 Symbol.toStringTag 属性,值为 Module。可通过判断 Symbol.toStringTag 是否为 Module 来判断。

如果是 import 语句,它的 default 才是需要的组件。

h 函数还能接收第二个参数 props 和第三个参数 slots,两者都可在 setup 的第二个参数中获取。

vue
<script setup lang="ts">
import { h } from 'vue'
import { defineAsyncComponent } from './index'

const AsyncComponent = defineAsyncComponent({
  loader: () => import('./test.vue'), 
  loadingComponent: () => h('div', 'loading...'),
  errorComponent: () => h('div', 'load error, please retry'),
})
</script>

<template>
  <AsyncComponent msg="父组件传递的信息">
    <div>父组件传递的插槽</div>
  </AsyncComponent>
</template>

<style scoped></style>
ts
import { h, shallowRef } from 'vue'

export const defineAsyncComponent = (options) => {
  // 不再考虑options的类型做不同的使用,如果它是函数,那么就转换为一个对象
  if (typeof options === 'function') {
    options = { loader: options }
  }
  const defineComponent = () => h('div', '') // 默认占位组件

  const {
    loader,
    loadingComponent = defineComponent,
    errorComponent = defineComponent,
    timeout = 5000,
  } = options

  // 再写一个函数,返回一个new Promise。这样就能手动修改new Promise的状态
  const loaderComponent = () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('加载超时')
      }, timeout)
      loader().then(resolve, reject)
    })
  }

  return {
    // 获取到 slots 和 attrs参数
    setup(_, { slots, attrs }) {
      // 这是一个函数式组件
      const component = shallowRef(loadingComponent)
      // 执行回调函数,获取组件
      loaderComponent()
        .then((res) => {
          if (res && res[Symbol.toStringTag] === 'Module') {
            component.value = res.default
          } else {
            component.value = res
          }
        })
        .catch((err) => {
          component.value = errorComponent
        })
      return () => {
        return h(component.value, attrs, slots) 
      }
    },
  }
}
vue
<script setup lang="ts">
interface Props {
  msg?: string
}

withDefaults(defineProps<Props>(), {
  msg: 'Hello World',
})
</script>

<template>
  <div>
    <h1>Child component: Test</h1>
    <p>{{ msg }}</p>
    <slot></slot>
  </div>
</template>

<style scoped></style>

动手实操

跳转预览:点击跳转

贡献者

The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共