Vue.js设计与实现-异步组件与函数式组件

开发 前端
本文中主要介绍了异步组件要解决的几个问题,如何解决这几个问题,怎么设计与实现?

1.写在前面

异步组件,其实和异步请求数据一样,只不过是通过异步加载的方式去加载和渲染组件。异步组件有什么作用,它可以用于代码分割和服务端下发组件等场景。函数式组件其实允许普通函数定义组件,将函数返回值作为组件渲染的内容。函数式组件最大的特点就是无状态。

2异步组件要解决的问题

同步渲染:

import App from "App.vue";
createApp(App).mount("#app");

异步渲染:

const loader = ()=>import("App.vue");
loader().then(App=>{
createApp(App).mount("#app");
})

上面代码中,通过动态导入的方式实现加载组件,会返回一个Promise实例,组件加载成功后会调用createApp函数完成挂载,从而实现异步渲染。

问题很多的小明就会问:上面代码中不是实现的是异步渲染整个页面吧,只需要渲染部分页面那么应该如何处理呢?

答案是:只需要实现异步加载部分组件。

<template>
<CompA/>
<component :is="asyncCompB"/>
</template>
<script>
import { shallowRef } from "vue";
import { CompA } from "CompA.vue";
export default{
components:{
CompA
},
setup(){
const asyncCompB = shallowRef(null);
import("CompB.vue").then(CompB=>asyncCompB.value=CompB);
return {
asyncCompB
}
}
}
</script>

上面代码中,CompA是同步加载的组件,CompB是异步加载的组件,通过动态组件绑定变量进行渲染的。

对于异步组件也要和异步请求数据一样处理异步存在的一些问题:

  • 组件加载失败或加载超时,显示Error组件
  • 组件加载时,显示Loading组件或占位
  • 组件加载超时显示Loading组件
  • 组件加载失败后可以进行请求重试

3.异步组件的实现原理

封装defineAsyncComponent接口

在上一节的代码中,进行异步加载组件的使用方式并不简单,对此为了降低复杂度进行封装defineAsyncComponent接口。defineAsyncComponent函数是个高阶函数,输入输出都是组件,输出的返回值是包装后的组件。

function defineAsyncComponent(loader){
let InnerComp = null;
return {
name:"AsyncComponentWrapper",
setup(){
// 异步加载成功的标识符
const loaded = ref(false);
loader().then(comp=>{
InnerComp = comp;
// 加载成功后
loader.value = true;
});
return ()=>{
return loaded.value ? { type: InnerComp } : { type: Text, children: "" }
}
}
}
}

上面代码中,defineAsyncComponent函数会根据加载器loader的状态决定渲染内容,成功加载组件则渲染被加载的组件,否则显示占位内容。

超时处理与Error组件

异步加载组件和异步请求数据一样,会存在弱网加载时间长的情况,对此需要在组件加载时间超过指定时长后触发超时错误。

const AsyncComp = defineAsyncComponent({
loader:()=>import("CompA"),
timeout:2000,//ms
// 出错时要渲染的组件
errorComponent:MyErrorComponent
});
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);

loader().then(comp=>{
InnerComp = comp;
// 加载成功后
loader.value = true;
})// 捕获加载中的错误
.catch(err=>error.value = err);

let timer = null;
if(options.timeout){
timer = setTimeout(()=>{
const err = new Error("异步组件将在${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{
return placeholder;
}
}
}
}
}

在上面代码中,加载器添加catch捕获加载错误,在加载超时后创建一个新的错误对象,并将其赋值给error.value变量。在组件渲染时只要有error.value的值存在,且配置了errorComponent,就直接渲染errorComponent组件并将error.value的值作为组件的props传递。这样就可以在自定义的Error组件上,通过定义名为error的proprs接收错误对象。

延迟与Loading组件

前面知道异步组件和异步请求一样会受到网络影响,对此进行了超时和Error处理,在加载过程中可以设置Loading组件提供更好的用户体验。Loading的展示时机是什么时候,如何控制它的显隐,对此可以添加一个延迟时间,在加载超过指定时间才显示Loading组件。

const AsyncComp = defineAsyncComponent({
loader:()=>new Promise(res=>/*...*/),
delay:200,//ms
// loading要渲染的组件
loadingComponent:{
setup(){
return ()=>{
return { type:"h2",children:"Loading..." }
}
}
}
});

上面代码中,delay用于指定延迟展示Loading组件的时长,loadingComponent用于配置Loading组件。

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);

let loadingTimer = null;

if(options.delay){
loadingTimer = setTimeout(()=>{
loading.value = true;
}, options.delay)
}else{
loading.value = true
}

loader().then(comp=>{
InnerComp = comp;
// 加载成功后
loader.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("异步组件将在${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){
return {
type: options.loadingComponent
}
}else{
return placeholder;
}
}
}
}
}

在上面代码中,其实就是通过设置一个loading.value变量来标识是否正在加载中,如果制定了延迟时间则到时间后设置loading.value=true,否则直接设置。为了避免内存泄漏,无论组件是否加载成功,都需要将定时器进行清除,避免Loading组件在加载成功后也展示。

当然,在异步组件加载成功后,不仅要讲定时器进行清除,还需要对Loading组件进行卸载,对此需要修改unmount函数:

function unmount(vnode){
if(vnode.type === "string"){
vnode.children.forEach(comp=>unmount(comp));
return;
}else if(typeof vnode.type === "object"){
unmount(vnode.component.subtree);
return
}
const parent = vnode.el.parentVNode;
if(parent){
parent.removeChild(vnode.el);
}
}

重试机制

重试就是在加载出错时,可以重新发起加载组件的请求,提供开箱即用的重试机制对于使用者而言是很有必要的。对于组件加载出错的情况下,可以为使用者提供请求重试或直接抛出异常。

function defineAsyncComponent(options){

// 省略代码

// 记录重试次数
let retires = 0;
function load(){
return loader().catch(err=>{
if(options.onError){
return new Promise((resolve, reject)=>{
//重试
const retry = ()=>{
resolve(load());
retires++;
}
// 失败
const fail = ()=>{
reject(err)
}
options.onError(retry, fail, retries);
})
}else{
throw err
}
})
}

// 省略部分代码
}

4.函数式组件

函数式组件本质上返回值为虚拟DOM的普通函数,因为函数式组件的简单性,因此Vue.js3使用函数式组件。函数式组件自身没有状态,而是通过外部传递过来的props,对此需要给函数添加静态props属性。

function MyFunctionComp(props){
return { type:"h1",children:prop.name }
}
//定义props
MyFunctionComp.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(typeof type === Text){
//...文本节点
}else if(typeof type === Fragement){
//...片段
}else if(
typeof type === "object" || //有状态组件
typeof type === "function" //无状态组件
){
// vnode.type的值是选项对象,作为组件处理
if(!n1){
//挂载组件
mountComponent(n2, container, anchor);
}else{
//更新组件
patchComponent(n1, n2, anchor);
}
}
}

mountComponent函数代码:

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
}
}
}

在mountComponent函数中检查组件类型是函数式还是对象,对于函数式直接作为组件选项对象的render选项,将组件函数props作为组件的props。

5.写在最后

本文中主要介绍了异步组件要解决的几个问题,如何解决这几个问题,怎么设计与实现?在Vue.js3中提供了异步组件,与此同时介绍了异步组件的加载超时问题、异常处理和Loading、请求重试等,还讨论了函数式组件的实现逻辑。

责任编辑:武晓燕 来源: 前端一码平川
相关推荐

2022-04-25 07:36:21

组件数据函数

2022-04-14 09:35:03

Vue.js设计Reflect

2022-04-04 16:53:56

Vue.js设计框架

2022-05-03 21:18:38

Vue.js组件KeepAlive

2022-04-12 08:08:57

watch函数options封装

2022-04-01 08:08:27

Vue.js框架命令式

2017-07-11 18:00:21

vue.js数据组件

2022-04-17 09:18:11

响应式数据Vue.js

2022-04-16 13:59:34

Vue.jsJavascript

2020-09-16 06:12:30

Vue.js 3.0Suspense组件前端

2022-04-18 08:09:44

渲染器DOM挂载Vue.js

2022-04-03 15:44:55

Vue.js框架设计设计与实现

2022-04-11 08:03:30

Vue.jscomputed计算属性

2021-05-18 07:51:37

Suspense组件Vue3

2022-03-02 07:52:13

React类组件函数式组件

2022-04-05 16:44:59

系统Vue.js响应式

2022-05-25 11:24:25

CalendarNutUI移动端

2022-04-09 17:53:56

Vue.js分支切换嵌套的effect

2021-02-10 07:31:12

VuejsElementUI

2019-05-29 14:23:53

Vue.js组件通信
点赞
收藏

51CTO技术栈公众号