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

      // 省略部分代码
    }
  }
}
相关文章
|
14天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
117 64
|
14天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
14天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
24 8
|
13天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
17 1
|
13天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
25 1
|
14天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
17天前
|
JavaScript 前端开发 API
从Vue 2到Vue 3的演进
从Vue 2到Vue 3的演进
31 0
|
17天前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
19天前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
|
20天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
23 1
vue学习第一章