13.2.3 延迟与 Loading 组件
网络好时异步组件瞬间加载完成,Loading 组件刚渲染完就被卸载会出现闪烁;解法是给它一个延迟展示时间,并在加载结束时无条件清掉延迟定时器。
一、为什么要延迟展示
- 异步加载的组件受网络影响较大,加载过程可能很快,也可能很慢;
- 展示 Loading 组件能提供更好的用户体验,但展示时机需要仔细考虑;
- 网络状况良好时,Loading 组件刚完成渲染就立即进入卸载阶段 → 出现闪烁;
- 所以给 Loading 组件设置一个延迟时间,例如「超过 200ms 没有完成加载,才展示 Loading 组件」。
二、实现思路
思路与「超时时长 + Error 组件」类似,关键点有四个:
| 关键点 | 做法 |
|---|---|
| 标记是否正在加载 | 新增变量 loading |
| 延迟展示 | 配置了 delay 就开一个延迟定时器,到点后把 loading.value 置为 true |
| 清理定时器 | 无论加载成功与否都要清掉延迟定时器 |
| 渲染 | 正在加载且用户指定了 Loading 组件时,渲染该组件 |
漏掉「无论成功与否都要清理」这一步,就会出现组件已经加载成功,界面上却还挂着 Loading 的问题。
// 一个标志,代表是否正在加载
const loading = ref(false)
let loadingTimer = null
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
// 没有配置 delay,则直接标记为加载中
loading.value = true
}
加载结束后统一收尾,用 finally 保证成功、失败两种情况都执行:
loader()
.then(c => {
InnerComp = c.default
loaded.value = true
})
.catch(err => (error.value = err))
.finally(() => {
// 加载完毕后,无论成功与否都要清除延迟定时器
loading.value = false
clearTimeout(loadingTimer)
})
三、渲染优先级
渲染函数按顺序判断,命中即返回:
| 条件 | 渲染内容 |
|---|---|
loaded.value | 被加载的组件 |
error.value && options.errorComponent | Error 组件,并把 error 作为 props 传入 |
loading.value && options.loadingComponent | Loading 组件 |
| 其余情况 | 占位内容 |
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
// 正在加载且用户指定了 Loading 组件
return { type: options.loadingComponent }
} else {
return placeholder
}
}
四、卸载 Loading 组件的本质
加载成功后要卸载 Loading 组件、渲染真正的组件,理解这一点需要回到渲染器:
- 组件的卸载,本质上是要卸载组件所渲染的内容,即 subTree;
- 通过组件实例的
vnode.component属性拿到组件实例,再递归调用unmount(vnode.component.subTree)。
function unmount(vnode) {
// Fragment 需要卸载其 children
if (vnode.type === Fragment) {
vnode.children.forEach(c => unmount(c))
return
}
// vnode.type 是对象,说明描述的是组件
if (typeof vnode.type === 'object') {
// 调用组件实例的 unmounted 钩子
vnode.component.unmounted.forEach(hook => hook())
// 卸载组件:真正要卸载的是它渲染出的 subTree
unmount(vnode.component.subTree)
return
}
const parent = vnode.el.parentNode
if (parent) parent.removeChild(vnode.el)
}
包装组件的渲染函数在 Loading 组件与异步组件之间切换
type,patch 发现type不同 → 走「卸载旧的、挂载新的」,于是 Loading 组件的 subTree 就被这一步自动卸掉了。
五、完整实现
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = { loader: options }
}
const { loader } = options
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = ref(null)
// 延迟展示 Loading 组件
const loading = ref(false)
let loadingTimer = null
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
loading.value = true
}
loader()
.then(c => {
InnerComp = c.default
loaded.value = true
})
.catch(err => (error.value = err))
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
error.value = new Error(`Async component timed out after ${options.timeout}ms.`)
}, options.timeout)
}
onUnmounted(() => clearTimeout(timer))
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
return { type: options.loadingComponent }
} else {
return placeholder
}
}
}
}
}
使用示例(1s 后才显示 Loading,2s 后判定超时):
const CompVNode = {
type: defineAsyncComponent({
loader: () =>
new Promise(resolve => {
setTimeout(() => resolve(import('../CompA.mjs')), 3000)
}),
timeout: 2000,
errorComponent: MyErrorComp,
delay: 1000, // 延迟 1000ms 展示 Loading 组件
loadingComponent: {
setup() {
return () => {
return { type: 'h5', children: 'Loading...' }
}
}
}
})
}
六、易错点
delay定时器必须在finally里清理,否则「加载已完成」与「延迟到时置loading = true」会打架,界面残留 Loading。
未配置
delay时要把loading.value直接置为true,否则默认false会让 Loading 组件永远不显示。
Loading 组件同样是组件(示例里用
setup()返回渲染函数),它被卸载时卸载的是自己的subTree,所以 Loading 组件内部可以随意写结构。
相关
- 13.2.4 重试机制:加载失败后不立即放弃,把重试的决定权交给用户