异步组件原理是利用了动态import
与静态导入不同,动态导入只在需要时进行。并返回一个满足模块名称空间对象的Promise
:一个包含模块中所有导出内容
的对象。例如将下面同步渲染改成异步渲染.只不过是改了一种导入方式,在.then之后执行操作
import App from './App'
createApp(App).mount(#app)
import('./App').then((App)=>{
createApp(App).mount('#app')
})
解析源码:
源码目录running-core/src/apiAsyncComponent
可以看出defineAsyncComponent
的入参source
,这是一个联合类型(AsyncComponentLoader,AsyncComponentOptions)。AsyncComponentLoader
的类型是一个返回promise
的函数,并且这个promise的解决值是模块导入的返回值。AsyncComponentOptions
是一个对象,里面包含着AsyncComponentLoader
类型。
loader函数的作用就是动态加载组件
loader = () => import('App.vue')
因此,入参可以是一个函数,也可以是一个对象。对于对象中的其他属性在下面逐一讲解。
export type AsyncComponentResolveResult<T = Component> = T | {
default: T } // es modules
export type AsyncComponentLoader<T = any> = () => Promise<
AsyncComponentResolveResult<T>
>
export interface AsyncComponentOptions<T = any> {
loader: AsyncComponentLoader<T>
loadingComponent?: Component//加载组件
errorComponent?: Component//错误组件
delay?: number//延时时间
timeout?: number//超时时间
//加载出错时,用户可以自己决定是否重新加载,它接收4个参数
//错误原因,重试,抛出错误方法,以及重试次数
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
source: AsyncComponentLoader<T> | AsyncComponentOptions<T>
先来实现一个最简单的defineAsyncComponent
可以看出它返回了一个高阶组件,在它的setup函数中设置一个标记loaded
,加载成功再设为true,这样根据这个标记判断返回组件还是占位组件。
// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
function defineAsyncComponent(loader) {
// 一个变量,用来存储异步加载的组件
let InnerComp = null
// 返回一个包装组件
return {
name: 'AsyncComponentWrapper',
setup() {
// 异步组件是否加载成功
const loaded = ref(false)
// 执行加载器函数,返回一个 Promise 实例
// 加载成功后,将加载成功的组件赋值给 InnerComp,并将 loaded 标记为 true,代表加载成功
loader().then(c => {
InnerComp = c
loaded.value = true
})
return () => {
// 如果异步组件加载成功,则渲染该组件,否则渲染一个占位内容
return loaded.value ? {
type: InnerComp } : {
type: Text, children: '' }
}
}
}
}
还应该提供超时服务,即超过了一定时间显示错误组件
- 通过官方文档可知
defineAsyncComponent
可以接受一个函数,或者一个对象(包含函数,超时时间,用来替换的错误组件等),这就需要对参数进行格式化 - 进入setup函数时创建一个定时器,定时时间即传入的超时时间,设置初始
超时标志
为flase,定时器结束设为true.并在卸载时销定时器
,避免内存泄漏。 - 最后根据这个超时标识渲染内容,如果没有传错误组件,这里是渲染
placeholder
,即空白文本 另外,
.then中的内容是一个微任务
,同步任务结束之后才会进行,此时我们的定时器已经开始。function defineAsyncComponent(options) { // options 可以是配置项,也可以是加载器 if (typeof options === 'function') { // 如果 options 是加载器,则将其格式化为配置项形式 options = { loader: options } } const { loader } = options let InnerComp = null return { name: 'AsyncComponentWrapper', setup() { const loaded = ref(false) // 代表是否超时,默认为 false,即没有超时 const timeout = ref(false) loader().then(c => { InnerComp = c loaded.value = true }) let timer = null if (options.timeout) { // 如果指定了超时时长,则开启一个定时器计时 timer = setTimeout(() => { // 超时后将 timeout 设置为 true timeout.value = true }, options.timeout) } // 包装组件被卸载时清除定时器 onUmounted(() => clearTimeout(timer)) // 占位内容 const placeholder = { type: Text, children: '' } return () => { if (loaded.value) { // 如果组件异步加载成功,则渲染被加载的组件 return { type: InnerComp } } else if (timeout.value) { // 如果加载超时,并且用户指定了 Error 组件,则渲染该组件 return options.errorComponent ? { type: ions.errorComponent } : placeholder } return placeholder } } } }
现在只解决了超时的错误,但是其他原因也可能导致错误。而且还想知道发生错误的原因是什么
- 要想知道发生了其他错误可以在
catch
中捕获 - 如果传入了错误组件。想知道错误原因可以将错误信息作为
props
传递给错误组件,便于用户分析问题
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = {
loader: options
}
}
const {
loader } = options
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
// 定义 error,当错误发生时,用来存储错误对象
const error = shallowRef(null)
loader()
.then(c => {
InnerComp = c
loaded.value = true
})
// 添加 catch 语句来捕获加载过程中的错误
.catch(err => (error.value = err))
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
// 超时后创建一个错误对象,并复制给 error.value
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) {
// 只有当错误存在且用户配置了 errorComponent 时才展示 Error组件,同时将 error 作为 props 传递
return {
type: options.errorComponent, props: {
error: error.value } }
} else {
return placeholder
}
}
}
}
}
异步加载的组件可能很慢,这个时候就需要Loading组件
,但是如果异步组件的加载速度很快,Loading组件将被快速的挂载再卸载,这会造成闪屏
。所以应该设置一段时间之后如果异步组件还没有加载好再挂载Loading组件。
- 首先还是应该设置一个
标识
,初始值为false,代表没有进入加载中,即Loading组件不应该存在。还需要设置一个定时器
,如果定时器结束设置标识为true,渲染Loading组件。 - 当异步组件加载完成,定时器也应该在promise的
finally
中清除 - 如果参数中没有传递超时时间,不需要定时器,直接将标识直接设为true
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
loaded.value = true
})
.catch(err => (error.value = err))
.finally(() => {
loading.value = false
// 加载完毕后,无论成功与否都要清除延迟定时器
clearTimeout(loadingTimer)
})
let timer = null
if (options.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 }
} else {
return placeholder
}
}
}
}
}
另外由于之前定义的这些标识都是响应式数据,所以当它们发生改变,组件会自动重新渲染.
接下来还要考虑如果加载失败,是否自动重试,自动重试的次数呢。
举个例子,如果请求一个接口超时了,那么自动再发送三次请求,否则不再发送。
- 用定时器模拟接口请求失败
- 封装
load
方法,在catch中捕获错误,并返回new Promise实例 - 并把该实例的 resolve 和 reject 方法暴露给用户,让用户来决定下一步应该怎么做
- 接着执行onError中有着用户自己的判断逻辑,决定重试还是取消,因为onError传递了retry,fail作为参数。
function fetch() {
console.log("try");
return new Promise((resolve, reject) => {
// 请求会在 1 秒后失败
setTimeout(() => {
reject("err");
}, 1000);
});
}
//记录次数
let count = 0;
// load 函数接收一个 onError 回调函数
function load(onError) {
// 请求接口,得到 Promise 实例
const p = fetch();
// 捕获错误
return p.catch((err) => {
// 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
// 同时将 retry 函数作为 onError 回调的参数
return new Promise((resolve, reject) => {
// retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
const retry = () => {
count++;
resolve(load(onError));
};
const fail = () => reject(err);
onError(retry, fail, count);
});
});
}
load((retry, fail, count) => {
if (count < 3) {
retry();
} else {
fail();
}
}).catch((err) => {
});
类比到异步组件上,我们该如何调整呢
- 要想触发重试,首先应该知道它什么时候发生了错误,即在传入的
loader
函数中catch
捕获错误 - 在catch中捕获到错误之后,判断用户是否传入了
onError
(用户自定义的错误处理函数,接受的参数有retry和fail
方法),如果不存在,直接抛出错误,否则返回一个新的Promise
实例,在实例中,封装retry和fail
,就是将实例的resolve,和reject分别封装进retry和fail
- 在retry中应该重新执行loader。应该将loader函数封装进一个新的函数load,便于复用代码。在load函数中也是
return loader().catch...
,也就是retry中重新执行load - 因为在catch中返回了一个Promise,所以load函数执行,进行重试也就是执行resolve,可以在.then将加载的标志设为true,也就是显示加载组件。这就是为什么返回promise实例的原因
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = {
loader: options
}
}
const {
loader } = options
let InnerComp = null
// 记录重试次数
let retries = 0
// 封装 load 函数用来加载异步组件
function load() {
return (
loader()
// 捕获加载器的错误
.catch(err => {
// 如果用户指定了 onError 回调,则将控制权交给用户
if (options.onError) {
// 返回一个新的 Promise 实例
return new Promise((resolve, reject) => {
// 重试
const retry = () => {
resolve(load())
retries++
}
// 失败
const fail = () => reject(err)
// 作为 onError 回调函数的参数,让用户来决定下一步怎么做
options.onError(retry, fail, retries)
})
} else {
throw error
}
})
)
}
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = shallowRef(null)
const loading = ref(false)
let loadingTimer = null
if (options.delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, options.delay)
} else {
loading.value = true
}
// 调用 load 函数加载组件
load()
.then(c => {
InnerComp = c
loaded.value = true
})
.catch(err => {
error.value = err
})
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
// 省略部分代码
}
}
}