Vue3之异步组件实现原理

简介: Vue3之异步组件实现原理

异步组件原理是利用了动态import

  1. 静态导入

  2. 动态导入(像一个函数的表达式)

与静态导入不同,动态导入只在需要时进行。并返回一个满足模块名称空间对象的Promise:一个包含模块中所有导出内容的对象。例如将下面同步渲染改成异步渲染.只不过是改了一种导入方式,在.then之后执行操作

import App from './App'
createApp(App).mount(#app)
import('./App').then((App)=>{
   
      createApp(App).mount('#app')
})

解析源码:

源码目录running-core/src/apiAsyncComponent

可以看出defineAsyncComponent的入参source,这是一个联合类型(AsyncComponentLoader,AsyncComponentOptions)。AsyncComponentLoader的类型是一个返回promise的函数,并且这个promise的解决值是模块导入的返回值。AsyncComponentOptions是一个对象,里面包含着AsyncComponentLoader类型。

loader函数的作用就是动态加载组件

loader = () => import('App.vue')

因此,入参可以是一个函数,也可以是一个对象。对于对象中的其他属性在下面逐一讲解。

export type AsyncComponentResolveResult<T = Component> = T | {
    default: T } // es modules

export type AsyncComponentLoader<T = any> = () => Promise<
  AsyncComponentResolveResult<T>
>
export interface AsyncComponentOptions<T = any> {
   
  loader: AsyncComponentLoader<T>
  loadingComponent?: Component//加载组件
  errorComponent?: Component//错误组件
  delay?: number//延时时间
  timeout?: number//超时时间
  //加载出错时,用户可以自己决定是否重新加载,它接收4个参数
  //错误原因,重试,抛出错误方法,以及重试次数
  onError?: (
    error: Error,
    retry: () => void,
    fail: () => void,
    attempts: number
  ) => any
}

source: AsyncComponentLoader<T> | AsyncComponentOptions<T>

先来实现一个最简单的defineAsyncComponent

可以看出它返回了一个高阶组件,在它的setup函数中设置一个标记loaded,加载成功再设为true,这样根据这个标记判断返回组件还是占位组件。

// defineAsyncComponent 函数用于定义一个异步组件,接收一个异步组件加载器作为参数
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: '' }
      }
    }
  }
}

还应该提供超时服务,即超过了一定时间显示错误组件

  1. 通过官方文档可知defineAsyncComponent可以接受一个函数,或者一个对象(包含函数,超时时间,用来替换的错误组件等),这就需要对参数进行格式化
  2. 进入setup函数时创建一个定时器,定时时间即传入的超时时间,设置初始超时标志为flase,定时器结束设为true.并在卸载时销定时器,避免内存泄漏。
  3. 最后根据这个超时标识渲染内容,如果没有传错误组件,这里是渲染placeholder,即空白文本
  4. 另外,.then中的内容是一个微任务,同步任务结束之后才会进行,此时我们的定时器已经开始。

    function defineAsyncComponent(options) {
         
    // options 可以是配置项,也可以是加载器
    if (typeof options === 'function') {
         
     // 如果 options 是加载器,则将其格式化为配置项形式
     options = {
         
       loader: options
     }
    }
    
    const {
          loader } = options
    
    let InnerComp = null
    
    return {
         
     name: 'AsyncComponentWrapper',
     setup() {
         
       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: ions.errorComponent }
             : placeholder
         }
         return placeholder
       }
     }
    }
    }
    

    现在只解决了超时的错误,但是其他原因也可能导致错误。而且还想知道发生错误的原因是什么

  5. 要想知道发生了其他错误可以在catch中捕获
  6. 如果传入了错误组件。想知道错误原因可以将错误信息作为props传递给错误组件,便于用户分析问题
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
        }
      }
    }
  }
}

异步加载的组件可能很慢,这个时候就需要Loading组件,但是如果异步组件的加载速度很快,Loading组件将被快速的挂载再卸载,这会造成闪屏。所以应该设置一段时间之后如果异步组件还没有加载好再挂载Loading组件。

  1. 首先还是应该设置一个标识,初始值为false,代表没有进入加载中,即Loading组件不应该存在。还需要设置一个定时器,如果定时器结束设置标识为true,渲染Loading组件。
  2. 当异步组件加载完成,定时器也应该在promise的finally中清除
  3. 如果参数中没有传递超时时间,不需要定时器,直接将标识直接设为true
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)
      // 一个标志,代表是否正在加载,默认为 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: '' }

      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 }
        } else {
   
          return placeholder
        }
      }
    }
  }
}

另外由于之前定义的这些标识都是响应式数据,所以当它们发生改变,组件会自动重新渲染.

接下来还要考虑如果加载失败,是否自动重试,自动重试的次数呢。

举个例子,如果请求一个接口超时了,那么自动再发送三次请求,否则不再发送。

  1. 用定时器模拟接口请求失败
  2. 封装load方法,在catch中捕获错误,并返回new Promise实例
  3. 并把该实例的 resolve 和 reject 方法暴露给用户,让用户来决定下一步应该怎么做
  4. 接着执行onError中有着用户自己的判断逻辑,决定重试还是取消,因为onError传递了retry,fail作为参数。
function fetch() {
   
  console.log("try");
  return new Promise((resolve, reject) => {
   
    // 请求会在 1 秒后失败
    setTimeout(() => {
   
      reject("err");
    }, 1000);
  });
}
//记录次数
let count = 0;
// load 函数接收一个 onError 回调函数
function load(onError) {
   
  // 请求接口,得到 Promise 实例
  const p = fetch();
  // 捕获错误
  return p.catch((err) => {
   
    // 当错误发生时,返回一个新的 Promise 实例,并调用 onError 回调,
    // 同时将 retry 函数作为 onError 回调的参数
    return new Promise((resolve, reject) => {
   
      // retry 函数,用来执行重试的函数,执行该函数会重新调用 load 函数并发送请求
      const retry = () => {
   
        count++;
        resolve(load(onError));
      };
      const fail = () => reject(err);
      onError(retry, fail, count);
    });
  });
}

load((retry, fail, count) => {
   
  if (count < 3) {
   
    retry();
  } else {
   
    fail();
  }
}).catch((err) => {
   });

类比到异步组件上,我们该如何调整呢

  1. 要想触发重试,首先应该知道它什么时候发生了错误,即在传入的loader函数中catch捕获错误
  2. 在catch中捕获到错误之后,判断用户是否传入了onError(用户自定义的错误处理函数,接受的参数有retry和fail方法),如果不存在,直接抛出错误,否则返回一个新的Promise实例,在实例中,封装retry和fail,就是将实例的resolve,和reject分别封装进retry和fail
  3. 在retry中应该重新执行loader。应该将loader函数封装进一个新的函数load,便于复用代码。在load函数中也是return loader().catch...,也就是retry中重新执行load
  4. 因为在catch中返回了一个Promise,所以load函数执行,进行重试也就是执行resolve,可以在.then将加载的标志设为true,也就是显示加载组件。这就是为什么返回promise实例的原因
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)
        })

      // 省略部分代码
    }
  }
}
相关文章
|
5天前
|
前端开发 JavaScript API
基于Vue3+Hooks实现4位随机数和60秒倒计时
本文介绍了如何在Vue3中使用Hooks API来实现生成4位随机数和执行60秒倒计时的功能,并提供了详细的代码示例和运行效果展示。
23 1
基于Vue3+Hooks实现4位随机数和60秒倒计时
|
1天前
|
JavaScript 前端开发 开发者
Vue学习之--------深入理解Vuex、原理详解、实战应用(2022/9/1)
这篇文章详细介绍了Vuex的基本概念、使用场景、安装配置、基本用法、实际应用案例以及注意事项,通过一个数字累加器的实战示例,帮助开发者深入理解Vuex的原理和应用。
|
1天前
|
JSON JavaScript 前端开发
Vue3在工作中使用的一些经验总结
这篇文章是关于Vue 3项目中使用TypeScript的一些经验总结,包括如何配置TSLint进行代码规范和类型检查,以及如何将现有的JavaScript代码迁移到TypeScript的步骤和注意事项。
Vue3在工作中使用的一些经验总结
|
4天前
|
JavaScript 算法 API
Vue 3有哪些新特性
【8月更文挑战第16天】Vue 3有哪些新特性
25 1
|
5天前
|
JavaScript UED
如何在Vue3项目中使用防抖节流技巧
在Vue 3项目中使用防抖和节流技巧以优化组件性能,包括使用`lodash`库和自定义实现这两种方法。
10 0
如何在Vue3项目中使用防抖节流技巧
|
1天前
|
JavaScript 前端开发 API
Vue3入门
Vue3入门
4 0
|
4天前
|
JavaScript
创建 Vue3 项目
创建 Vue3 项目
11 0
|
4天前
|
JavaScript
如何创建一个Vue项目(手把手教你)
这篇文章是一篇手把手教读者如何创建Vue项目的教程,包括使用管理员身份打开命令行窗口、找到存放项目的位置、通过vue-cli初始化项目、填写项目信息、进入项目目录、启动项目等步骤,并提供了一些常见第三方库的引入方法。
如何创建一个Vue项目(手把手教你)
|
1天前
|
JavaScript
Vue学习之--------路由的query、params参数、路由命名(3)(2022/9/5)
这篇文章详细介绍了Vue路由中的query参数、命名路由、params参数以及props配置的使用方式,并通过实际项目案例展示了它们在开发中的应用和测试结果,同时解释了`<router-link>`的`replace`属性如何影响浏览器历史记录。
Vue学习之--------路由的query、params参数、路由命名(3)(2022/9/5)
|
1天前
|
JavaScript
Vue学习之--------VueX(2022/8/31)
这篇文章是关于VueX的基础知识介绍,涵盖了VueX中的state、mutations、getters和actions的定义和使用,以及Action提交mutation而非直接变更状态,以及mutations的同步执行特性。
Vue学习之--------VueX(2022/8/31)