1.异步组件需要解决的问题
同步组件和异步组件
//同步渲染
import App from 'App.vue'
createApp(App).mount('#app')
//异步渲染
const loader = ()=> import('App.vue')
loader().then(App=>{
createApp(App).mount('#app')
})
异步渲染时,使用动态语句import()来加载组件,会返回一个Promise。组件加载成功后,会调用createApp函数完成挂载,这样就实现了以异步的方式来渲染页面
下面是一个App.vue组件
<template>
<CompA />
<component :is="asyncComp" />
</template>
<script>
import { shallowRef } from 'vue'
import CompA from 'CompA.vue'
export default {
components: { CompA },
setup() {
const asyncComp = shallowRef(null)
// 异步加载 CompB 组件
import('CompB.vue').then(CompB => asyncComp.value = CompB)
return {
asyncComp
}
}
}
CompA组件就是同步渲染的,而动态组件绑定了asyncComp变量;在script里面,通过动态语句import()来异步加载CompB组件,当asyncComp变量的值设置为CompB。这样实现了CompB组件的异步加载和渲染
用户虽然可以这样进行异步组件的加载渲染,但是很麻烦,需要考虑以下问题:
1.组件加载失败,是否渲染Error组件
2.组件加载时是否需要占位,例如loading组件
3.异步组件加载时有延迟,怎么解决
4.组件加载失败后,是否需要重试
vue给的解决方法,一一对应:
1.允许用户指定加载出错时要渲染的组件
2.允许用户指定loading组件,以及展示该组件的延迟时间
3.允许用户设置加载组件的超时时长
4.组件加载失败时,为用户提供重试的能力。
2.异步组件的实现原理
1.封装 defineAsyncComponent 函数
异步组件本质是通过封装手段来实现很好的用户接口,从而降低用户层面的使用复杂度,如下所示
<template>
<AsyncComp />
</template>
<script>
export default {
components: {
// 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
AsyncComp: defineAsyncComponent(() => import('CompA'))
}
}
</script>
使用defineAsyncComponent来定义组件,并直接使用components组件选项来注册它。
defineAasyncComponent是一个高阶组件,基本实现如下:
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 函数本质上是一个高阶组件,它的返回值是一个包装组件。
● 包装组件会根据加载器的状态来决定渲染什么内容。如果加载器成功地加载了组件,则渲染被加载的组件,否则会渲染一个占位内容。
● 通常占位内容是一个注释节点。组件没有被加载成功时,页面中会渲染一个注释节点来占位。但这里我们使用了一个空文本节点来占位。
2.超时与Error组件
加载一个异步组件的事件并不是可以预知的,需要指定要一个Error组件。当加载超出设定的预期值时,渲染Error组件
设置一个接口,用户可以指定一个超时时长和超时后渲染的组件
const AsyncComp = defineAsyncComponent({
loader: () => import('CompA.vue'),
timeout: 2000, // 超时时长,其单位为 ms
errorComponent: MyErrorComp // 指定出错时要渲染的组件
})
异步组件内处理超时问题
function defineAsyncComponent(options) {
// options 可以是配置项,也可以是加载器
if (typeof options === 'function') {
// 如果 options 是加载器,则将其格式化为配置项形式
options = {
loader: options
}
}
const { loader } = options
let InnerComp = null
return {
name: 'AsyncComponentWrapper',
setup() {
//代表组件是否成功加载,初始为false,即没有
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: options.errorComponent } : placeholder
}
return placeholder
}
}
}
}
流程:
1.设置一个变量,代表异步组件加载是否超时,默认false
2.开始加载组件时,开启定时器。一旦超出设定值,将超时变量设置为true。定时器需要在包装组件卸载时清除。
3.包装组件会根据标识变量来决定渲染的内容。加载成功,渲染被夹在的组件。加载超时,就渲染Error组件。
不过,超时只是发生错误的一种,应该还要兼容一些其他错误场景:
1.当错误发生时,把错误对象作为Error组件的props传过去,以便用户后续能自行进行更细粒度的处理。
2.除了超时之外,有能力处理其他原因导致的加载错误,例如网络失败等。
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
}
}
}
}
}
3.延时与Loading组件
异步组件受网络影响大,加载过程快慢很难确定。如果有loading组件,在加载完成之前,可以展示loading组件,给用户一个好的体验体验。
但异步组件如果很快就加载完成,又会出现闪屏的情况。所以需要设置一个延时展示的事件,当规定时间内没有完成,才展示Loading组件。
用户接口设计如下:
defineAsyncComponent({
loader: () => new Promise(r => { /* ... */ }),
// 延迟 200ms 展示 Loading 组件
delay: 200,
// Loading 组件
loadingComponent: {
setup() {
return () => {
return { type: 'h2', children: 'Loading...' }
}
}
}
})
异步组件关于Loading组件的处理
function defineAsyncComponent(options) {
//判断options是加载器
if (typeof options === 'function') {
options = {
loader: options
}
}
//loader为组件加载器
const { loader } = options
//变量=>组件记载成功后的值
let InnerComp = null
return {
//组件名称
name: 'AsyncComponentWrapper',
//setup函数
setup() {
//是否挂载完毕的标志
const loaded = ref(false)
//用来承载Error组件的变量
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: '' }
//setup函数返回结果
return () => {
if (loaded.value) {
//如果正常完成渲染,无错误无超时,返回正确的渲染组件
return { type: InnerComp }
} else if (error.value && options.errorComponent) {
//如果有是触发了Error组件这条逻辑,就返回Error组件,并且错误信息会放到props当中
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
// 如果异步组件正在加载,并且用户指定了 Loading 组件,则渲染 Loading 组件
return { type: options.loadingComponent }
} else {
//占位符兜底
return placeholder
}
}
}
}
}
上面的函数逻辑流程如下:
1.需要一个标记变量loading来代表组件是否正在加载
2.如果用户制定了延时时间,则开启延时定时器。定时器到时后,再将loading.value的值设置为true
3.无论组件加载成功与否,都要清除延时定时器,否则会出现组件已经加载成功,但仍然展示Loading组件的问题。
4.在渲染函数中,如果组件正在加载,并且用户制定了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)
}
}
对于组件的卸载,本质上是卸载组件所渲染的内容,即subTree。所以在上面的代码中,通过组件实例的vnode.component属性得到组件实例,再递归调用unmount函数完成vnode.compoennt.subTree的卸载。
3.重试机制
重试是指加载出错时,用能力重新发起加载组件的请求。在组件加载过程中,发生错误的情况很常见,所以需要提供开箱的重试机制,可以提升用户体验。
异步组件加载失败后的重试机制,与请求服务端口失败后的重试机制一样。
模拟接口请求
function fetch() {
return new Promise((resolve, reject) => {
// 请求会在 1 秒后失败
setTimeout(() => {
reject('err')
}, 1000);
})
}
假设调用fetch函数会发送HTTP请求,并且该请求会在1秒后失败。为了实现失败后的重试,需要封装一个load函数:
function load(onError){
//请求接口,得到Promise实例
const p = fetch()
//捕获错误
return p.patch(err=>{
//当错误发生时,返回一个新的Promise实例,并调用onError回调
//同时将retry函数作为onError回调的参数
return new Promise((resolve,reject)=>{
//retry函数,用来执行重试的函数,执行该函数会重新调用load函数并发送请求
const retry = ()=>resolve(load(onError))
const fail = ()=>reject(err)
onError(retry,fail)
})
})
}
load函数内部调用了fetch函数来发送请求,并得到一个Promise实例。接着,用catch语句来捕获错误。捕获到错误时:
1.抛出错误,返回一个Promise实例,并且把该实例的resolve和reject方法暴露给用户,让用户来决定下一步该怎么做。
2.将新的Promise实例的resolve和reject分别封装为retry函数和fail函数,将它们作为onError回调函数的参数。
这里选择了第二个方案,用户可以在错误发生时主动选择重试或直接抛出错误。
展示用户如何进行重试加载的:
load((retry)=>{
//失败后重试
retry()
}).then(res=>{
//成功
console.log(res)
})
异步组件的重试机制
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)
})
// 省略部分代码
}
}
}
4.函数式组件
一个函数式组件,就是一个普通函数,该函数的返回值是虚拟DOM。
在用户接口层面,一个函数式组件就是返回一个虚拟DOM的函数:
function MyFuncComp(props){
return {type:'h1,',children:props.title}
}
函数式组件没有自身状态,但它仍然可以接受外部传入的props。为了给函数式组件定义props,需要在函数组件上添加静态的props:
function MyFuncComp(props) {
return { type: 'h1', children: props.title }
}
// 定义 props
MyFuncComp.props = {
title: String
}
在有状态组件的基础上,实现函数式组件将变得非常简单,因为挂载组件的逻辑可以复用mountComponent函数。为此,需要在patch函数㕯支持函数类型的vnode.type:
function patch(n1, n2, container, anchor) {
if (n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (typeof type === 'string') {
// 省略部分代码
} else if (type === Text) {
// 省略部分代码
} else if (type === Fragment) {
// 省略部分代码
} else if (
// type 是对象 --> 有状态组件
// type 是函数 --> 函数式组件
typeof type === 'object' || typeof type === 'function'
) {
// component
if (!n1) {
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
无论是有状态组件,还是函数式组件,都可以通过mountComponent函数来完成挂载,也都可以通过patchComponent函数来完成更新。
function mountComponent(vnode, container, anchor) {
// 检查是否是函数式组件
const isFunctional = typeof vnode.type === 'function'
let componentOptions = vnode.type
if (isFunctional) {
// 如果是函数式组件,则将 vnode.type 作为渲染函数,将 vnode.type.props 作为 props 选项定义即可
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
// 省略部分代码
}
在mountComponent函数内检查组件的类型,如果是函数式组件,则直接将组件函数作为组件选项对象的render选项,并将组件函数的静态props属性作为组件的props选项即可,其他逻辑保持不变。