Vue3之异步组件实现原理

异步组件利用了动态import,下面链接指向对应的文档。

  1. 静态导入(The static import declaration)

  2. 动态导入(dynamic import, is a function-like expression)

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

js 复制代码
import App from './App'
createApp(App).mount(#app)
js 复制代码
import('./App').then((App)=>{
      createApp(App).mount('#app')
})

这么来说用户也可以自己封装异步组件,但要实现一个完善的异步组件并不轻松。

看看官方的实现defineAsyncComponent

defineAsyncComponent 方法接收一个返回 Promise 的加载函数,得到的是一个封装之后的组件,props 和插槽还是能正常传给内部组件

先来实现一个最简单的defineAsyncComponent

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

js 复制代码
// 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中的内容是一个微任务,同步任务结束之后才会进行,此时我们的定时器已经开始。
js 复制代码
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
      }
    }
  }
}

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

  1. 要想知道发生了其他错误可以在catch中捕获
  2. 如果传入了错误组件。想知道错误原因可以将错误信息作为props传递给错误组件,便于用户分析问题
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)
      // 定义 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
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)
      // 一个标志,代表是否正在加载,默认为 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
        }
      }
    }
  }
}
相关推荐
拾光拾趣录28 分钟前
Element Plus表格表头动态刷新难题:零闪动更新方案
前端·vue.js·element
Adolf_19931 小时前
React 中 props 的最常用用法精选+useContext
前端·javascript·react.js
前端小趴菜051 小时前
react - 根据路由生成菜单
前端·javascript·react.js
喝拿铁写前端1 小时前
`reduce` 究竟要不要用?到底什么时候才“值得”用?
前端·javascript·面试
空の鱼1 小时前
js与vue基础学习
javascript·vue.js·学习
鱼樱前端1 小时前
2025前端SSR框架之十分钟快速上手Nuxt3搭建项目
前端·vue.js
極光未晚1 小时前
React Hooks 中的时空穿梭:模拟 ComponentDidMount 的奇妙冒险
前端·react.js·源码
Codebee1 小时前
OneCode 3.0 自治UI 弹出菜单组件功能介绍
前端·人工智能·开源
ui设计兰亭妙微1 小时前
# 信息架构如何决定搜索效率?
前端
1024小神2 小时前
Cocos游戏中UI跟随模型移动,例如人物头上的血条、昵称条等
前端·javascript