13. 异步组件与函数式组件
在 Vue.js 3 中使用函数式组件,主要是因为它的简单性,而不是因为它的性能好
13.1 异步组件要解决的问题
- 允许用户指定加载出错时要渲染的组件。
- 允许用户指定 Loading 组件,以及展示该组件的延迟时间。
- 允许用户设置加载组件的超时时长。
- 组件加载失败时,为用户提供重试的能力。
13.2 异步组件的实现原理
1. 封装 defineAsyncComponent
函数
html
<template>
<AsyncComp />
</template>
<script>
export default {
components: {
// 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
AsyncComp: defineAsyncComponent(() => import("CompA")),
},
};
</script>
js
// 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: "" };
};
},
};
}
2. 超时与 Error 组件
js
const AsyncComp = defineAsyncComponent({
loader: () => import("CompA.vue"),
timeout: 2000, // 超时时长,其单位为 ms
errorComponent: MyErrorComp, // 指定出错时要渲染的组件
});
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);
}
// 包装组件被卸载时清除定时器
onUmounted(() => clearTimeout(timer));
// 占位内容
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 组件更能提升用户体验
js
defineAsyncComponent({
loader: () =>
new Promise((r) => {
/* ... */
}),
// 延迟 200ms 展示 Loading 组件
delay: 200,
// Loading 组件
loadingComponent: {
setup() {
return () => {
return { type: "h2", children: "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);
// 一个标志,代表是否正在加载,默认为 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;
}
};
},
};
}
修改 unmount
函数以支持异步组件的卸载
js
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);
}
}
4. 重试机制
先看下接口重试
js
function fetch() {
return new Promise((resolve, reject) => {
// 请求会在 1 秒后失败
setTimeout(() => {
reject("err");
}, 1000);
});
}
// 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 = () => resolve(load(onError));
const fail = () => reject(err);
onError(retry, fail);
});
});
}
// 调用 load 函数加载资源
load(
// onError 回调
(retry) => {
// 失败后重试
retry();
}
).then((res) => {
// 成功
console.log(res);
});
组件重试
js
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);
});
// 省略部分代码
},
};
}
13.3 函数式组件
一个函数式组件就是一个返回虚拟 DOM 的函数,函数式组件没有自身状态,但它仍然可以接收由外部传入的 props
js
function MyFuncComp(props) {
return { type: "h1", children: props.title };
}
// 定义 props
MyFuncComp.props = {
title: String,
};
在 patch
函数内部,通过检测 vnode.type
的类型来判断组件的类型
js
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
函数来完成更新
js
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,
};
}
// 省略部分代码
}