13.vue3中异步组件defineAsyncComponent实现原理

1.异步组件的基本使用

1.1异步组件的定义

在Vue中,当我们注册全局或局部组件时,它们都是同步地被"立即解析并加载"的。这意味着在我们的程序初始化时,所有组件都会通过网络被下载到内存中,并且在内存中占用一定的资源。预加载所有组件会将页面的初始加载时间和性能降低,尤其是在移动设备上。为了避免这种情况,Vue.js 提供了异步组件

1.2异步的用法

关于异步的用法,可以去看看vue的文档,简单介绍下

1.ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:

js 复制代码
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

2.异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:

js 复制代码
const AsyncComp = defineAsyncComponent({
  // 加载函数
  loader: () => import('./Foo.vue'),
  // 加载异步组件时使用的组件
  loadingComponent: LoadingComponent,
  // 展示加载组件前的延迟时间,默认为 200ms
  delay: 200,
  // 加载失败后展示的组件
  errorComponent: ErrorComponent,
  // 如果提供了一个 timeout 时间限制,并超时了
  // 也会显示这里配置的报错组件,默认值是:Infinity
  timeout: 3000
})

2.异步组件的实现原理

2.1 封装defineAsyncComponent函数

我们从上面了解了异步函数的使用,那我们接下来就来实现defineAsyncComponent函数,其实从本质上面来说,异步组件是一个高阶组件,通过封装,方便用户更好的调用 我们举个简单的例子:

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

我们可以使用defineAsyncComponent来定义异步组件,并再components中注册它,这样,再模版中就可以想使用普通组件一样使用异步组件。我们理解defineAsyncComponent本质上是一个高阶组件,它的基本实现如下:

js 复制代码
  function defineAsyncComponent(loader) {
      let innerComp = null //变量,用来存储异步加载的组件
      return {
          name: 'AsyncComponentWrapper',
          setup() {
              const loaded = ref(false) //判断组件是否成功
              loader().then(c => {
                  innerComp = c
                  loaded.value = true
              })
              //成功则返回组件,否则渲染一个占位符
              return loaded.value ? { type:innerComp}:{type:'Text',children:''}
          }
      }
  }

关键点:

  • defineAsyncComponent函数本质上是一个高阶组件,它的返回值是一个包装组件
  • 包装组件根据加载状态渲染什么内容,成功就加载组件,否则返回一个占位符

2.2 超时与error处理

再上面处理中,我们就完成了异步组件的基础实现,但是我们考虑,异步组件通常通过网络请求,那加载组件可能就需要时间,而且加载时间过长,会触发超时的错误,这个时候我们也得提供Error组件配置功能。

所以我们针对于defineAsyncComponent函数需要接受一个配置对象为参数:

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

知道我们接受一个对象后,我们就需要对defineAsyncComponent函数进行改造了,具体的实现如下:

js 复制代码
function defineAsyncComponent(options) {
    if (typeof options === 'function') { //新增options判断处理
        options = { loader: options }
    }
    const { loader } = options
    let innerComp = null
    return {
        name: 'AsyncComponentWrapper',
        setup() {
            const loaded = ref(false)
            const timeout = ref(false) //表示是否超时,默认没有超时
            loader().then(c => {
                innerComp = c
                loaded.value = true
            })
            if (options.timeout) { //新增超时逻辑
                timer = setTimeout(() => {
                    timeout.value = true
                }, options.timeout)
            }
            // 包装组件被卸载时清除定时器
            onUmounted(() => clearTimeout(timer))
            // 占位内容
            const placeholder = { type: Text, children: '' }

            return () => {
                if (loaded.value) {
                    return { type: innerComp }
                } else if (timeout.value) { //新增超时渲染逻辑
                    return options.errorComponent ? { type: options.errorComponent } : placeholder
                }
                return placeholder
            }
        }
    }
}
  • 我们新增了一个timeout标识,用来标识异步组件是否已经超时
  • 开始加载组件的同时,开始定时器进行计时。当加载超时后,将timeout的值设置为true,表示已经超时。
  • 包装组件啊根据loader和timeout的值来决定具体渲染内容。如果异步组件成功,那么渲染加载组件,如果异步组件超时,并且用户指定了Error组件,则渲染Error组件

2.3延迟和loading组件

异步加载的组件受网络影响较大,可能很快,也可能很慢,所以我们考虑是否一个loading组件来提供更好的一言,一般情况下,我们从加载的时刻就应该展示loading。但是再网络好的情况下,异步组件加载会很快,会导致loading组件刚渲染完成,就立即卸载,会出现闪烁的情况,我们我们很自然的想到,就采用一个loading延迟展示的功能,比如,当超过200ms没有加载完成,我们才展示loading组件。

使用的代码如下:

js 复制代码
 defineAsyncComponent({
   loader: () => new Promise(r => { /* ... */ }),
   // 延迟 200ms 展示 Loading 组件
   delay: 200,
   // Loading 组件
   loadingComponent: {
     setup() {
       return () => {
         return { type: 'h2', children: 'Loading...' }
       }
     }
   }
 })
  • delay:用来指定延迟展示loading组件的市场
  • loadingComponent,用来配置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) // 用于存储加载错误信息

            const loading = ref(false) // 新增用于存储加载状态
            const loadingTimer = null //新增
            if (options.delay) { //新增delay逻辑,如果存在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 // 加载完成后设置加载状态为 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)
            }
            onUmounted(() => clearTimeout(timer))
            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}
                }
                return placeholder
            }
        }
    }
}
  • 我们新增了一个loading标记变量,来代表组件是否正在加载
  • 如果用户指定了延迟时间delay,则开启延迟定时器
  • 无论组件是否加载成功,都要清除延迟定时器,否则会出现组件已经记载成功,仍展示loading组件的问题
  • 如果再渲染函数中,如果组件正在加载,并且用户指定了loading组件,则渲染loading组件。

3.总结

异步组件在页面性能、拆包以及服务端下发组件等场景中尤为重要,从本质上来说就是一个高阶组件。*我们只是从内部封装完善了它的相关特性:

  1. 再加载出错时要渲染的组件
  2. 可以指定loading组件,以及展示该组件的延迟时间
  3. 可以设置组件加载的超时时长
相关推荐
dog shit4 分钟前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
我有一只臭臭4 分钟前
el-tabs 切换时数据不更新的问题
前端·vue.js
七灵微8 分钟前
【前端】工具链一本通
前端
Nueuis1 小时前
微信小程序前端面经
前端·微信小程序·小程序
_r0bin_4 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君4 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
zhang98800004 小时前
JavaScript 核心原理深度解析-不停留于表面的VUE等的使用!
开发语言·javascript·vue.js
potender4 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11084 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂5 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler