13.2.1 封装 defineAsyncComponent 函数
defineAsyncComponent是一个高阶组件:接收异步组件的加载器,返回一个「包装组件」,由包装组件根据加载状态决定渲染加载到的组件还是占位内容。
一、整体思路
用 defineAsyncComponent 定义异步组件,比在 13.1 里自己写 shallowRef + import().then 简单直接得多:
<template>
<CompA />
<component :is="asyncComp" />
</template>
<script>
import CompA from 'CompA.vue'
export default {
components: { CompA },
setup() {
const asyncComp = defineAsyncComponent(() => import('CompB.vue'))
return { asyncComp }
}
}
</script>
关键点只有三个:
defineAsyncComponent的返回值是一个包装组件,用户拿到的就是这个组件;- 包装组件根据加载器的状态决定渲染什么:加载成功 → 渲染被加载的组件,否则 → 渲染占位内容;
- 占位内容通常是一个注释节点(也可以用空文本节点),即页面里先什么都不显示。
二、最小实现
function defineAsyncComponent(loader) {
// 用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
name: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 执行加载器函数,返回一个 Promise 实例
loader().then(c => {
// import() 返回的是模块对象,真正的组件在 default 属性上
InnerComp = c.default
loaded.value = true
})
return () => {
// 加载成功则渲染该组件,否则渲染占位内容
return loaded.value
? { type: InnerComp }
: { type: Text, children: '' }
}
}
}
}
使用:
const CompVNode = {
type: defineAsyncComponent(() => import('../CompA.mjs'))
}
renderer.render(CompVNode, document.querySelector('#app'))
三、为什么能自动切换
- 包装组件的渲染函数里读取了
loaded.value→ 渲染函数与loaded建立了响应式联系; - 异步加载成功把
loaded.value = true→ 触发包装组件自更新 → 重新执行渲染函数 → 这次type变成了InnerComp; - 渲染器 patch 时发现新旧 vnode 的
type不同(占位节点 → 组件),于是卸载占位内容、挂载真正的组件,切换全自动完成。
四、加载器为什么要包一层函数
- 早期写法可以直接接收
import('CompB.vue')返回的 Promise 实例,但 Promise 只能被消费一次; - 后续要支持重试(失败后重新发起加载请求),就必须把加载动作包成函数
() => import('CompB.vue'),每次调用都得到一个新的 Promise。
因此
defineAsyncComponent的入参逐步从「Promise 实例」演进为「加载器函数」。
五、易错点
loader 里 resolve 出来的是模块对象(
import()的结果是模块命名空间),直接把它当组件用会渲染出错,必须取default(兼容 CJS 打包产物时可用c.__esModule ? c.default : c)。
占位内容必须是合法的 vnode,不能随随便便返回
null;用空文本节点或注释节点都可以,它们同样会被渲染器正常 patch。
包装组件名(如
AsyncComponentWrapper)只是便于调试,真正决定行为的是它setup返回的渲染函数。
相关
- 13.2.2 超时与 Error 组件:给包装组件补上超时判定与出错时的兜底渲染