异步组件要解决的问题
异步组件完全可以由用户自行实现:
// 异步渲染
import loader = () => import('App.vue')
loader().then(App => {
createApp(App).mount('#app')
})
// 异步渲染部分页面
<script>
import CompA from 'CompA'
const asyncComp = shallowRef(null)
import('CompB.vue').then(CompB => asyncComp.value = CompB)
</script>
<template>
<CompA />
<component :is="asyncComp" />
</template>
但为了更好地解决问题, 需要在框架层面提供更好的支持.
- 允许用户指定加载出错时要渲染的组件
- 允许用户指定Loading组件, 以及展示该组件的延迟时间
- 允许用户设置加载组件的超时时长
- 组件加载失败时, 提供重试的能力
异步组件的实现原理
封装defineAsyncComponent函数, 用来定义异步组件. 本质上是一个高阶组件, 返回值是一个包装组件. 以下是要支持的几项:
- 超时与Error组件
- 延迟与Loading组件
- 重试机制
// 设计接口, 然后实现
const AsyncComp = defineAsyncComponent({
loader: () => import('CompA.vue'),
// 超时时长
timeout: 2000,
// 出错时加载的组件
errorComponent: ErrorComp,
// 延迟展示Loading组件的时长
delay: 200,
// 可能会存在加载速度很快, 没有必要再展示Loading组件的情况
loadingComponent: {
setup(){
return { type: 'h2', children: 'Loading ...' }
}
}
})
// 实现异步组件方法
function defineAsyncComponent(options){
if(typeof options === 'function') {
options = { loader: options }
}
const { loader, errorComponent, timeout, onError, delay } = options
let InnerComp = null
// 记录重试次数
let retries = 0
// 重试机制
// 将 retry 和 fail 作为onError回调的参数, 让用户控制重试或抛出错误
function load(){
return loader().catch(err => {
if(onError) {
return new Promise((resolve, reject) => {
const retry = () => {
resolve(load())
retries++
}
const fail = () => reject(err)
onError(retry, fail, retries)
})
} else {
throw err
}
})
}
return {
name: 'AsyncComponentWrapper',
setup(){
const loaded = ref(false), error = shallowRef(false), loading = ref(false)
let loadingTimer = null
// 延迟控制 到达指定时间才显示Loading组件
if(delay) {
loadingTimer = setTimeout(() => {
loading.value = true
}, delay)
} else {
loading.value = true
}
// 调用 load 加载组件
load().then(c => {
InnerComp = c
loaded.value = true
}).catch(err => {
error.value = err
}).finally(() => {
loading.value = false
clearTimout(loadingTimer)
})
// 超时控制
let timer = null
if(timeout){
timer = setTimeout(() => {
const err = new Error('Async Component timed out after ${timeout}ms.')
error.value = err
}, timeout)
}
onUnmounted(() => clearTimout(timer))
const placeholder = { type: Text, children: '' }
return () => {
if(loaded.value) {
return { type: InnerComp }
} else if(error.value && errorComponent){
return { type: errorComponent, props: error.value }
} else if(loading.value && loadingComponent){
return { type: loadingComponent }
}
return placeholder
}
}
}
}
函数式组件
函数式组件的本质是一个函数, 其返回值是虚拟dom. 且没有自身状态, 但可以接收由外部传入的props. patch函数内部通过vnode.type的类型判断组件的类型:
- 如果
vnode.type是一个对象, 则它是一个有状态组件, 并且vnode.type是组件选项对象. - 如果
vnode.type是一个函数, 则它是一个函数式组件.
// 函数式组件
function FuncComp(props){
return { type: 'h1', children: props.title }
}
FuncComp.props = {
title: String
}
// 增加对函数式组件的支持
function mountComponent(){
const isFunctional = typeof vnode.type === 'function'
let componentOptions = vnode.type
// 对函数式组件的处理
if(isFunctional) {
componentOptions = {
render: vnode.type,
props: vnode.type.props
}
}
// ...
}
isFunctional变量实现选择性地执行初始化逻辑, 因为函数式组件无须初始化data以及生命周期钩子.
总结
- 异步组件在页面性能、拆包以及服务端下发组件等场景尤为重要.