13.异步组件和函数式组件

发布于:2025-07-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

1.异步组件需要解决的问题

同步组件和异步组件

//同步渲染
import App from 'App.vue'
createApp(App).mount('#app')


//异步渲染
const loader = ()=> import('App.vue')
loader().then(App=>{
    createApp(App).mount('#app')
})

异步渲染时,使用动态语句import()来加载组件,会返回一个Promise。组件加载成功后,会调用createApp函数完成挂载,这样就实现了以异步的方式来渲染页面

下面是一个App.vue组件

  <template>
   <CompA />
   <component :is="asyncComp" />
 </template>
 <script>
 import { shallowRef } from 'vue'
 import CompA from 'CompA.vue'

 export default {
   components: { CompA },
   setup() {
     const asyncComp = shallowRef(null)

     // 异步加载 CompB 组件
     import('CompB.vue').then(CompB => asyncComp.value = CompB)

     return {
       asyncComp
     }
   }
 }

CompA组件就是同步渲染的,而动态组件绑定了asyncComp变量;在script里面,通过动态语句import()来异步加载CompB组件,当asyncComp变量的值设置为CompB。这样实现了CompB组件的异步加载和渲染

用户虽然可以这样进行异步组件的加载渲染,但是很麻烦,需要考虑以下问题:

1.组件加载失败,是否渲染Error组件

2.组件加载时是否需要占位,例如loading组件

3.异步组件加载时有延迟,怎么解决

4.组件加载失败后,是否需要重试

vue给的解决方法,一一对应:

1.允许用户指定加载出错时要渲染的组件

2.允许用户指定loading组件,以及展示该组件的延迟时间

3.允许用户设置加载组件的超时时长

4.组件加载失败时,为用户提供重试的能力。

2.异步组件的实现原理

1.封装 defineAsyncComponent 函数

异步组件本质是通过封装手段来实现很好的用户接口,从而降低用户层面的使用复杂度,如下所示

<template>
   <AsyncComp />
 </template>
 <script>
 export default {
   components: {
     // 使用 defineAsyncComponent 定义一个异步组件,它接收一个加载器作为参数
     AsyncComp: defineAsyncComponent(() => import('CompA'))
   }
 }
 </script>

使用defineAsyncComponent来定义组件,并直接使用components组件选项来注册它。

defineAasyncComponent是一个高阶组件,基本实现如下:

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

● defineAsyncComponent 函数本质上是一个高阶组件,它的返回值是一个包装组件。

● 包装组件会根据加载器的状态来决定渲染什么内容。如果加载器成功地加载了组件,则渲染被加载的组件,否则会渲染一个占位内容。

● 通常占位内容是一个注释节点。组件没有被加载成功时,页面中会渲染一个注释节点来占位。但这里我们使用了一个空文本节点来占位。

2.超时与Error组件

加载一个异步组件的事件并不是可以预知的,需要指定要一个Error组件。当加载超出设定的预期值时,渲染Error组件

设置一个接口,用户可以指定一个超时时长和超时后渲染的组件

 const AsyncComp = defineAsyncComponent({
   loader: () => import('CompA.vue'),
   timeout: 2000, // 超时时长,其单位为 ms
   errorComponent: MyErrorComp // 指定出错时要渲染的组件
 })

异步组件内处理超时问题

function defineAsyncComponent(options) {
   // options 可以是配置项,也可以是加载器
   if (typeof options === 'function') {
     // 如果 options 是加载器,则将其格式化为配置项形式
     options = {
       loader: options
     }
   }

   const { loader } = options

   let InnerComp = null

   return {
     name: 'AsyncComponentWrapper',
     setup() {
        //代表组件是否成功加载,初始为false,即没有
       const loaded = ref(false)
       // 代表是否超时,默认为 false,即没有超时
       const timeout = ref(false)

       loader().then(c => {
         InnerComp = c
         loaded.value = true
       })

       let timer = null
       if (options.timeout) {
         // 如果指定了超时时长,则开启一个定时器计时
         timer = setTimeout(() => {
           // 超时后将 timeout 设置为 true
           timeout.value = true
         }, options.timeout)
       }
       // 包装组件被卸载时清除定时器
       onUmounted(() => clearTimeout(timer))

       // 占位内容
       const placeholder = { type: Text, children: '' }

       return () => {
         if (loaded.value) {
           // 如果组件异步加载成功,则渲染被加载的组件
           return { type: InnerComp }
         } else if (timeout.value) {
           // 如果加载超时,并且用户指定了 Error 组件,则渲染该组件
           return options.errorComponent ? { type: options.errorComponent } : placeholder
         }
         return placeholder
       }
     }
   }
 }

流程:

1.设置一个变量,代表异步组件加载是否超时,默认false

2.开始加载组件时,开启定时器。一旦超出设定值,将超时变量设置为true。定时器需要在包装组件卸载时清除。

3.包装组件会根据标识变量来决定渲染的内容。加载成功,渲染被夹在的组件。加载超时,就渲染Error组件。

不过,超时只是发生错误的一种,应该还要兼容一些其他错误场景:

1.当错误发生时,把错误对象作为Error组件的props传过去,以便用户后续能自行进行更细粒度的处理。

2.除了超时之外,有能力处理其他原因导致的加载错误,例如网络失败等。

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

       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组件,给用户一个好的体验体验。

但异步组件如果很快就加载完成,又会出现闪屏的情况。所以需要设置一个延时展示的事件,当规定时间内没有完成,才展示Loading组件。

用户接口设计如下:

 defineAsyncComponent({
   loader: () => new Promise(r => { /* ... */ }),
   // 延迟 200ms 展示 Loading 组件
   delay: 200,
   // Loading 组件
   loadingComponent: {
     setup() {
       return () => {
         return { type: 'h2', children: 'Loading...' }
       }
     }
   }
 })

异步组件关于Loading组件的处理

 function defineAsyncComponent(options) {
    //判断options是加载器
   if (typeof options === 'function') {
     options = {
       loader: options
     }
   }
   //loader为组件加载器
   const { loader } = options
   //变量=>组件记载成功后的值
   let InnerComp = null
   
   return {
    //组件名称
     name: 'AsyncComponentWrapper',   
     //setup函数
     setup() {
        //是否挂载完毕的标志
       const loaded = ref(false)
       //用来承载Error组件的变量
       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: '' }
     
       //setup函数返回结果
       return () => {
         if (loaded.value) {
            //如果正常完成渲染,无错误无超时,返回正确的渲染组件
           return { type: InnerComp }
         } else if (error.value && options.errorComponent) {  
            //如果有是触发了Error组件这条逻辑,就返回Error组件,并且错误信息会放到props当中
           return { type: options.errorComponent, props: { error: error.value } }
         } else if (loading.value && options.loadingComponent) {
           // 如果异步组件正在加载,并且用户指定了 Loading 组件,则渲染 Loading 组件
           return { type: options.loadingComponent }
         } else {
           //占位符兜底
           return placeholder
         }
       }
     }
   }
 }

上面的函数逻辑流程如下:

1.需要一个标记变量loading来代表组件是否正在加载

2.如果用户制定了延时时间,则开启延时定时器。定时器到时后,再将loading.value的值设置为true

3.无论组件加载成功与否,都要清除延时定时器,否则会出现组件已经加载成功,但仍然展示Loading组件的问题。

4.在渲染函数中,如果组件正在加载,并且用户制定了Loading组件,则渲染该Loading组件。

当异步组件加载成功后,会卸载Loading组件并渲染异步加载的组件。为了支持Loading组件的卸载,需要修改unmount函数

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

对于组件的卸载,本质上是卸载组件所渲染的内容,即subTree。所以在上面的代码中,通过组件实例的vnode.component属性得到组件实例,再递归调用unmount函数完成vnode.compoennt.subTree的卸载。

3.重试机制

重试是指加载出错时,用能力重新发起加载组件的请求。在组件加载过程中,发生错误的情况很常见,所以需要提供开箱的重试机制,可以提升用户体验。

异步组件加载失败后的重试机制,与请求服务端口失败后的重试机制一样。

模拟接口请求

function fetch() {
   return new Promise((resolve, reject) => {
     // 请求会在 1 秒后失败
     setTimeout(() => {
       reject('err')
     }, 1000);
   })
 }

假设调用fetch函数会发送HTTP请求,并且该请求会在1秒后失败。为了实现失败后的重试,需要封装一个load函数:

 function load(onError){
    //请求接口,得到Promise实例
    const p = fetch()
    //捕获错误
    return p.patch(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函数内部调用了fetch函数来发送请求,并得到一个Promise实例。接着,用catch语句来捕获错误。捕获到错误时:

1.抛出错误,返回一个Promise实例,并且把该实例的resolve和reject方法暴露给用户,让用户来决定下一步该怎么做。

2.将新的Promise实例的resolve和reject分别封装为retry函数和fail函数,将它们作为onError回调函数的参数。

这里选择了第二个方案,用户可以在错误发生时主动选择重试或直接抛出错误。

展示用户如何进行重试加载的:

 load((retry)=>{
    //失败后重试
    retry()
 }).then(res=>{
    //成功
    console.log(res)
 })

异步组件的重试机制

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

       // 省略部分代码
     }
   }
 }

4.函数式组件

一个函数式组件,就是一个普通函数,该函数的返回值是虚拟DOM。

在用户接口层面,一个函数式组件就是返回一个虚拟DOM的函数:

function MyFuncComp(props){
    return {type:'h1,',children:props.title}
}

函数式组件没有自身状态,但它仍然可以接受外部传入的props。为了给函数式组件定义props,需要在函数组件上添加静态的props:

function MyFuncComp(props) {
   return { type: 'h1', children: props.title }
 }
 // 定义 props
 MyFuncComp.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 (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函数来完成更新。

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

   // 省略部分代码
 }

在mountComponent函数内检查组件的类型,如果是函数式组件,则直接将组件函数作为组件选项对象的render选项,并将组件函数的静态props属性作为组件的props选项即可,其他逻辑保持不变。

总结

1730879355715


网站公告

今日签到

点亮在社区的每一天
去签到