1.异步组件的基本使用
1.1异步组件的定义
在Vue中,当我们注册全局或局部组件时,它们都是同步地被"立即解析并加载"的。这意味着在我们的程序初始化时,所有组件都会通过网络被下载到内存中,并且在内存中占用一定的资源。预加载所有组件会将页面的初始加载时间和性能降低,尤其是在移动设备上。为了避免这种情况,Vue.js 提供了异步组件
1.2异步的用法
关于异步的用法,可以去看看vue的文档,简单介绍下
1.ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
js
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
2.异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
js
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
2.异步组件的实现原理
2.1 封装defineAsyncComponent函数
我们从上面了解了异步函数的使用,那我们接下来就来实现defineAsyncComponent
函数,其实从本质上面来说,异步组件是一个高阶组件,通过封装,方便用户更好的调用
。 我们举个简单的例子:
js
<template>
<AsyncComp />
</template>
<script>
export default {
components: {
// 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
AsyncComp: defineAsyncComponent(() => import('CompA'))
}
}
</script>
我们可以使用defineAsyncComponent
来定义异步组件,并再components中注册它,这样,再模版中就可以想使用普通组件一样使用异步组件。我们理解defineAsyncComponent
本质上是一个高阶组件,它的基本实现如下:
js
function defineAsyncComponent(loader) {
let innerComp = null //变量,用来存储异步加载的组件
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false) //判断组件是否成功
loader().then(c => {
innerComp = c
loaded.value = true
})
//成功则返回组件,否则渲染一个占位符
return loaded.value ? { type:innerComp}:{type:'Text',children:''}
}
}
}
关键点:
defineAsyncComponent
函数本质上是一个高阶组件,它的返回值是一个包装组件- 包装组件根据加载状态渲染什么内容,成功就加载组件,否则返回一个占位符
2.2 超时与error处理
再上面处理中,我们就完成了异步组件的基础实现,但是我们考虑,异步组件通常通过网络请求,那加载组件可能就需要时间,而且加载时间过长,会触发超时的错误,这个时候我们也得提供Error组件配置功能。
所以我们针对于defineAsyncComponent
函数需要接受一个配置对象为参数:
js
const AsyncComp = defineAsyncComponent({
loader: () => import('CompA.vue'),
timeout: 2000, // 超时时长,其单位为 ms
errorComponent: MyErrorComp // 指定出错时要渲染的组件
})
知道我们接受一个对象后,我们就需要对defineAsyncComponent函数进行改造了,具体的实现如下:
js
function defineAsyncComponent(options) {
if (typeof options === 'function') { //新增options判断处理
options = { loader: options }
}
const { loader } = options
let innerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const timeout = ref(false) //表示是否超时,默认没有超时
loader().then(c => {
innerComp = c
loaded.value = true
})
if (options.timeout) { //新增超时逻辑
timer = setTimeout(() => {
timeout.value = true
}, options.timeout)
}
// 包装组件被卸载时清除定时器
onUmounted(() => clearTimeout(timer))
// 占位内容
const placeholder = { type: Text, children: '' }
return () => {
if (loaded.value) {
return { type: innerComp }
} else if (timeout.value) { //新增超时渲染逻辑
return options.errorComponent ? { type: options.errorComponent } : placeholder
}
return placeholder
}
}
}
}
- 我们新增了一个timeout标识,用来标识异步组件是否已经超时
- 开始加载组件的同时,开始定时器进行计时。当加载超时后,将timeout的值设置为true,表示已经超时。
- 包装组件啊根据loader和timeout的值来决定具体渲染内容。如果异步组件成功,那么渲染加载组件,如果异步组件超时,并且用户指定了Error组件,则渲染Error组件
2.3延迟和loading组件
异步加载的组件受网络影响较大,可能很快,也可能很慢,所以我们考虑是否一个loading组件来提供更好的一言,一般情况下,我们从加载的时刻就应该展示loading。但是再网络好的情况下,异步组件加载会很快,会导致loading组件刚渲染完成,就立即卸载,会出现闪烁的情况,我们我们很自然的想到,就采用一个loading延迟展示的功能,比如,当超过200ms没有加载完成,我们才展示loading组件。
使用的代码如下:
js
defineAsyncComponent({
loader: () => new Promise(r => { /* ... */ }),
// 延迟 200ms 展示 Loading 组件
delay: 200,
// Loading 组件
loadingComponent: {
setup() {
return () => {
return { type: 'h2', children: 'Loading...' }
}
}
}
})
- delay:用来指定延迟展示loading组件的市场
- loadingComponent,用来配置loading组件
那我们新增上面两个参数,我们就需要来改造我们的代码了
js
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) // 用于存储加载错误信息
const loading = ref(false) // 新增用于存储加载状态
const loadingTimer = null //新增
if (options.delay) { //新增delay逻辑,如果存在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 // 加载完成后设置加载状态为 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)
}
onUmounted(() => 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){
//新增 如果存在异步正在加载,并且有loading组件,则渲染loading组件
return { type: options.loadingComponent}
}
return placeholder
}
}
}
}
- 我们新增了一个loading标记变量,来代表组件是否正在加载
- 如果用户指定了延迟时间delay,则开启延迟定时器
- 无论组件是否加载成功,都要清除延迟定时器,否则会出现组件已经记载成功,仍展示loading组件的问题
- 如果再渲染函数中,如果组件正在加载,并且用户指定了loading组件,则渲染loading组件。
3.总结
异步组件在页面性能、拆包以及服务端下发组件等场景中尤为重要,从本质上来说就是一个高阶组件
。*我们只是从内部封装完善了它的相关特性:
- 再加载出错时要渲染的组件
- 可以指定loading组件,以及展示该组件的延迟时间
- 可以设置组件加载的超时时长