《Vue.js 设计与实现》读书笔记 - 第13章、异步组件与函数式组件
第13章、异步组件与函数式组件
13.1 异步组件要解决的问题
用户可以简单通过 import
异步导入组件。
<template>
<component :is="asyncComp">
</template>
<script>
export default {
setup() {
const asyncComp = shallowRef(null)
import ('CompB.vue').then(CompB => asyncComp.value = CompB)
}
}
</script>
但我们还需要考虑组件加载失败,Loading 显示,失败重试等问题。
13.2 异步组件的实现原理
卸载函数增加卸载组件的逻辑
function unmount(vnode) {
if (vnode.type === Fragment) {
vnode.children.forEach((c) => unmount(c))
return
} else if (typeof vnode.type === 'object') {
unmount(vnode.component.subTree)
return
}
const parent = vnode.el.parentNode
if (parent) {
parent.removeChild(vnode.el)
}
}
在异步组件增加完善的逻辑,具体含义见代码注释。
function defineAsyncComponent(options) {
if (typeof options === 'function') {
options = {
loader: options,
}
}
const { loader } = options
let InnerComp = null
let retries = 0
function load() {
return loader().catch((err) => {
if (options.onError) {
// 函数返回Promise 并让用户在retry中调用resolve
// 这样用户retry之后 通过load().then() 可以获取组件值
// 即使又失败 就是又返回一个Promise 对象 还是要获取Promise中resolve的值
// 所以这样可以多次调用retry
return new Promise((resolve, reject) => {
const retry = () => {
resolve(load())
retries++
}
const fail = () => reject(err)
options.onError(retry, fail, retries)
})
} else {
throw err
}
})
}
return {
name: 'AsyncComponentWrapper',
setup() {
const loaded = ref(false)
const error = shallowRef(null)
const loading = ref(false)
const loadingTimer = null
if (options.delay) {
// 设置options.delay的话,在options.delay时间之后再把loading设置为true,
// 防止组件加载比较快的情况下出现loading一闪而过导致用户体验差
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))
// 加载后设置加载成功 loading为false
.finally(() => {
loading.value = false
clearTimeout(loadingTimer)
})
let timer = null
if (options.timeout) {
timer = setTimeout(() => {
const err = new Error('异步组件加载超时')
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) {
// 存在错误返回error对应组件
return { type: options.errorComponent, props: { error: error.value } }
} else if (loading.value && options.loadingComponent) {
// 加载中返回loading对应组件
return { type: options.loadingComponent }
}
return placeholder
}
},
}
}
13.3 函数式组件
函数式组件本质就是一个普通函数,该函数的返回值是虚拟 DOM。在 Vue3 中函数式组件的性能和普通组件差不多,但是比较简单。
实现也很简单,就是在 patch
新增对函数的支持,同时在组件挂载时,把 type
作为渲染函数。
function patch(n1, n2, container, anchor) {
//...
// 新增了对函数的判断
if (typeof type === 'object' || typeof type === 'function') {
// 组件
if (!n1) {
// 挂载
mountComponent(n2, container, anchor)
} else {
patchComponent(n1, n2, anchor)
}
}
}
function mountComponent(vnode, container, anchor) {
const isFunctional = typeof vnode.type === 'function'
let componentOptions = vnode.type
if (isFunctional) {
componentOptions = {
render: vnode.type,
props: vnode.type.props,
}
}
// ...
}
使用方法:
function MyFuncComp(props) {
return {
type: 'h1',
children: props.title
}
}
MyFuncComp.props = {
title: String
}
const vnode = {
type: MyFuncComp,
props: {
title: '函数式组件'
}
}