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. 可以设置组件加载的超时时长
相关推荐
少糖研究所几秒前
ColorThief库是如何实现图片取色的?
前端
冴羽1 分钟前
SvelteKit 最新中文文档教程(22)—— 最佳实践之无障碍与 SEO
前端·javascript·svelte
ZYLAB3 分钟前
我写了一个简易的 SEO 教程,希望能让新手朋友看完以后, SEO 能做到 80 分
前端·seo
小桥风满袖9 分钟前
Three.js-硬要自学系列4 (阵列立方体和相机适配、常见几何体、高光材质、lil-gui库)
前端·css
深海丧鱼11 分钟前
什么!只靠前端实现视频分片?
前端·音视频开发
ohMyGod_12313 分钟前
Vue如何实现样式隔离
前端·javascript·vue.js
涵信17 分钟前
第二十节:项目经验-描述一个React性能优化案例
前端·react.js·性能优化
Danny_FD23 分钟前
前端中的浮动、定位与布局
前端·css
Abadbeginning26 分钟前
vue3后台管理框架geeker admin 横向布局(了解)
前端·javascript·vue.js
OpenTiny社区28 分钟前
直播分享|TinyVue 多端实战与轻量图标库分享
前端·vue.js·开源