异步组件加载过程可能很慢,所以可以通过展示Loading组件来提供更好的用户体验,这样用户就不会有“卡死”的感觉了。
但是展示Loading组件的时机是一个需要仔细考虑的问题。如果从加载开始的一开始就展示Loading组件,那么在网络状况好的情况下,异步组件的加载速度会非常快,这会导致Loading组件刚完成渲染就立即进入卸载阶段,于是出现闪烁的情况。这样体验特别不好,因此要为Loading组件设置一个延迟展示的时间。例如,当超过200ms没有完成加载,才展示Loading组件。这样就能避免闪烁问题。
但是,首先要考虑的仍然是用户接口的设计,如下面代码所示:
defineAsyncComponent({
loader: ()=>new Promise(r=>{/* ... */}),
// 延迟200ms 展示Loading组件
delay:200,
// loading组件
loadingComponent:{
setup(){
return ()=>{
return {type:'h2',children:'Loading...'}
}
}
}
})
然后可以着手实现延迟时间与Loading组件,代码如下:
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 = shallowRef(null)
// 标志是否正在加载,默认为false
const loading = ref(false)
let loadingTimer = null
// 如果配置项中存在delay,则开启一个定时器计时,当延迟到时候将loading.value设置为true
if(options.delay){
loadingTimer = setTimeout(()=>{
loading.value = true
}, options.delay);
}else{
// 如果配置中没有delay,则直接标记为加载中
loading.value = true
}
loader()
.then(c=>{
InnerComp = c
loader.value = true
})
.catch((err)=>{error.value = err}
.finally(()=>{
loading.value = false
// 加载完毕后,无论是否成功都要清除定时器
clearTimeout(loadingTimer)
})
let timer = null
if(option.timeout){
timer = setTimeout(()=>{
const err = new Error(`Async component timed out after ${options.timeout}ms.`)
error.value = err
}, options.timeout)
}
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){
// 如果异步组件正在加载,并且用户指定了Loading组件,则渲染Loading组件
return {type:options.loadingComponent}
}
return placeholder
}
}
}
}
要注意的是,在渲染函数中,如果组件正在加载,并且用户指定了Loading组件,则渲染该Loading组件
另外要注意,当异步组件加载成功后,会卸载Loading组件并渲染异步加载的组件。为了支持Loading组件的卸载,需要修改unmount函数,如下代码所示:
function unmount(vnode){
if(vnode.type === Fragment){
vnode.children.forEach(c => unmount(c))
return
}else if(typeof vnode.type === 'object'){
// 对于组件的卸载,本质上是要卸载组件所渲染的内容,即subTree
unmount(vnode.component.subTree)
return
}
const parent = vnode.el.parentNode
if(parent){
parent.removeChild(vnode.el)
}
}