深圳幻海软件技术有限公司 欢迎您!

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

2023-02-28

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

1.写在前面

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

2异步组件要解决的问题

同步渲染:

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

异步渲染:

const loader = ()=>import("App.vue");
loader().then(App=>{
  createApp(App).mount("#app");
})
  • 1.
  • 2.
  • 3.
  • 4.

上面代码中,通过动态导入的方式实现加载组件,会返回一个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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

上面代码中,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: "" }
      }
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

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

超时处理与Error组件

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

const AsyncComp = defineAsyncComponent({
  loader:()=>import("CompA"),
  timeout:2000,//ms
  // 出错时要渲染的组件
  errorComponent:MyErrorComponent
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
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;
        }
      }
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

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

延迟与Loading组件

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

const AsyncComp = defineAsyncComponent({
  loader:()=>new Promise(res=>/*...*/),
  delay:200,//ms
  // loading要渲染的组件
  loadingComponent:{
    setup(){
      return ()=>{
        return { type:"h2",children:"Loading..." }
      }
    }
  }
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

上面代码中,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;
        }
      }
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.

在上面代码中,其实就是通过设置一个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);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

重试机制

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

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
      }
    })
  }
  
  // 省略部分代码
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.

4.函数式组件

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

function MyFunctionComp(props){
  return { type:"h1",children:prop.name }
}
//定义props
MyFunctionComp.props = {
  title: String
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

在有状态组件的基础上,只需要在挂载组件的逻辑可以复用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);
    }
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.

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
    }
  }
}
  
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

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

5.写在最后

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