ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Vue源码阅读(34):异步组件的源码解析

2021-09-22 10:33:42  阅读:203  来源: 互联网

标签:异步 Vue res factory 34 源码 error 组件 加载


异步组件的官方文档点击这里

借助于异步组件,我们可以将 Vue 项目按照组件分割成一些小的代码块,并且让这些代码块在前端需要时才从服务器进行加载。这种优化措施在大型应用中是很有必要的,可以大大缩短首次加载的时间。

在这里,建议读者先将 Vue 官网中的异步组件部分复习一遍,了解异步组件的写法。

1,异步组件的加载时机

首先说明一下异步组件触发加载的时间,异步组件的加载是在执行 render 函数创建 vnode 的过程中,如果判断出当前创建 vnode 的组件是异步组件的话,则会执行 resolveAsyncComponent 方法到服务器请求该异步组件的资源,相关源码如下所示:

// 创建组件的 vnode
export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | void {
  ......

  // 异步组件的处理
  let asyncFactory
  // 下面的 if 代码块是处理异步组件的逻辑
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // 返回一个占位用的 VNode,将会被渲染成一个注释节点。
      // 但是这个 VNode 保留了渲染这个异步组件所需的所有信息数据
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  ... 本地组件的处理逻辑 ...
}

createComponent 方法用于创建组件对应的 vnode,当构造方法 Ctor 没有 cid 属性的时候,说明当前的组件是一个异步组件,此时需要借助 resolveAsyncComponent 方法去服务器请求该异步组件的资源,resolveAsyncComponent 方法的返回值是异步组件的构造函数,不过由于异步请求需要时间,所以这里同步代码获取的 Ctor 返回值有可能是 undefined,当 Ctor 是 undefined 的时候,Vue 的做法是返回一个注释站位用的 vnode。

当异步组件经过首次加载后,异步组件的资源会被缓存起来,此时,如果当前的异步组件再次被使用的话,上面的 resolveAsyncComponent 方法则会直接返回已经请求成功的异步组件资源,此时 resolveAsyncComponent 方法的返回值 Ctor 就不是 undefined 了,代码的执行逻辑会进入下面的本地组件的处理逻辑。

2,resolveAsyncComponent 方法的源码解析

resolveAsyncComponent 方法定义在 src/core/vdom/helpers/resolve-async-component.js 文件中,我在源码中写了大量的注释和个人理解,边看源码边看注释,即可较为轻松的理解,源码如下所示:

// 异步组件实现的本质是 2 次渲染,先渲染成注释节点,当组件加载成功后,再通过 forceRender 重新渲染
// 异步组件的写法
// 1:处理异步组件(工厂函数)
// 最终返回 该组件的构造函数
// Vue.component('HelloWorld', function (resolve, reject) {
//   // 这个特殊的 require 语法告诉 webpack
//   // 自动将编译后的代码分割成不同的块
//   // 下面的代码其实就是发送 ajax 请求,到后端获取这个组件的数据,
//   require(['./components/HelloWorld'], function (res) {
//     // 这个方法是发送 ajax 的回调函数,它是异步的,会在 ajax 请求完成后进行执行
//     // 它的执行时机要晚于同步代码
//     resolve(res)
//   })
// })

// 2:处理异步组件(工厂函数 + Promise)
// Vue.component('HelloWorld', () => import('./components/HelloWorld.vue'))

// 3:高级异步组件
// const AsyncComp = () => ({
//   // 需要加载的组件,应当是一个 Promise
//   component: import('./components/HelloWorld.vue'),
//   // 加载中应当渲染的组件
//   loading: LoadingComp,
//   // 出错时渲染的组件
//   error: ErrorComp,
//   // 渲染加载中组件前的等待时间。默认:200ms。
//   delay: 200,
//   // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
//   timeout: 1000
// })
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>,
  context: Component
): Class<Component> | void {
  // resolveAsyncComponent 函数会被多次触发执行,第一次执行,发送请求,获取异步组件的信息
  // 无论异步组件的信息是否正常获取,都会将相关信息赋值到 factory 上面,这里的相关信息包括
  // error、resolved、loading 等表示异步组件获取状态的变量,然后执行 forceRender 方法
  // 重新渲染,这会再次进入 resolveAsyncComponent 函数,此时就可以根据 error、resolved、loading
  // 等数据判断异步组件的加载状态,返回对应的组件信息
  //
  // 如果 factory.error 变量为 true 的话,说明异步组件加载失败了,此时需要判断 factory.errorComp
  // 有没有定义,如果定义了的话,则返回这个异步组件加载失败时应该显示的 error 组件
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    // 返回 error 组件
    return factory.errorComp
  }

  // 和上面同理,判断 factory.resolved 是否被定义,如果已经被定义的话,说明当前的异步组件加载成功
  // 此时返回这个异步组件的定义即可
  if (isDef(factory.resolved)) {
    return factory.resolved
  }

  // 和上面同理,异步组件的加载还有一个加载中的状态,并且可以定义对应的加载中组件,当异步组件正在加载中
  // 的时候,会显示这个加载中组件,源码实现就在这个地方
  //
  // 如果 factory.loading 为 true 并且 factory.loadingComp 被定义了的话,
  // 则返回加载中组件
  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  // 当第一次执行到这时,此时是当前的异步组件第一次被使用,factory.contexts 肯定没有被定义,
  // 代码会进入 else 的逻辑,在 else 的逻辑中,factory.contexts 会被定义,这个 contexts
  // 是一个数组,数组中存储使用了当前异步组件的 Vue 实例
  //
  // 下次执行到这时,说明当前的异步组件已经被使用了,factory.contexts 已经被定义,此时将当前的 Vue 实例
  // push 到 factory.contexts 数组中即可
  //
  // 那么这个 factory.contexts 数组有什么用呢?其实这个数组用于存储当前这个异步组件在加载中的时候,使用了
  // 这个异步组件的 Vue 实例,也就是组件,当这个异步组件加载成功或者失败时,可以触发 contexts 数组中所有
  // Vue 实例的 $forceUpdate 方法,强制这些使用了当前异步组件的组件重新渲染,进而渲染出这个已经加载完成了
  // 的异步组件。
  if (isDef(factory.contexts)) {
    // 将使用了当前异步组件的 Vue 实例 push 到 factory.contexts 数组中
    factory.contexts.push(context)
  } else {
    // 当前的异步组件第一次被使用时,代码会执行到这,此时需要初始化 factory.contexts
    // 初始化时的数据是 [context]
    const contexts = factory.contexts = [context]
    let sync = true

    // 创建一个工具方法 forceRender,它的作用是遍历 factory.contexts 数组中的 Vue 实例
    // 执行这个 Vue 实例的 $forceUpdate 方法,强制这些组件进行重新渲染
    const forceRender = () => {
      for (let i = 0, l = contexts.length; i < l; i++) {
        contexts[i].$forceUpdate()
      }
    }

    // 创建异步组件工厂函数的 resolve 参数,是一个函数类型
    const resolve = once((res: Object | Class<Component>) => {
      // 这里的 res 是请求获取到的异步组件对象,通过 ensureCtor 可以创建出对应的组件构造函数
      // 内部借助了 extend 方法
      // 将该异步组件的构造函数保存在 factory.resolved 上
      factory.resolved = ensureCtor(res, baseCtor)
      if (!sync) {
        // 异步组件已经通过 ajax 请求从后端获取到了,所以在这里需要对组件重新渲染
        // 将异步组件渲染到页面上
        forceRender()
      }
    })

    // 创建异步组件工厂函数的 reject 参数,是一个函数类型
    const reject = once(reason => {
      process.env.NODE_ENV !== 'production' && warn(
        `Failed to resolve async component: ${String(factory)}` +
        (reason ? `\nReason: ${reason}` : '')
      )
      if (isDef(factory.errorComp)) {
        // 如果定义了 errorComp 组件的话,在这里将 factory.error 设置为 true
        // 并强制组件重新渲染,当组件重新渲染时,在上面的代码中,会直接返回 errorComp
        factory.error = true
        forceRender()
      }
    })

    // 执行组件的工厂函数
    // 在组件的工厂函数中会执行这个组件的异步加载,通过发送 ajax 请求,
    // 获取组件的数据后,将组件的数据当做参数执行 resolve 方法,resolve 方法会进行组件的重新加载
    const res = factory(resolve, reject)

    if (isObject(res)) {
      // 下面的代码块是用于处理 Promise 情况的
      // 如果我们:Vue.component() 的写法是返回一个 Promise 的话,那么上面 factory 方法的返回值就是一个 Promise
      if (typeof res.then === 'function') {
        // () => import('./my-async-component')
        if (isUndef(factory.resolved)) {
          // 将 resolve 和 reject 回调函数注册到 Promise.then() 中
          // 这样当 ajax 请求完成,这个 Promise 就是 resolved 的状态,
          // 然后就会执行 resolve 这个回调函数,接下来的逻辑和上面的工厂函数就一样了。
          res.then(resolve, reject)
        }
      } else if (isDef(res.component) && typeof res.component.then === 'function') {
        // 下面的代码块是针对 高级异步组件 的情况,此时 res 是一个对象,并且 res.component 是一个 Promise
        // 注册 resolve 和 reject 回调函数
        res.component.then(resolve, reject)

        // 处理 高级异步组件 中的 error
        if (isDef(res.error)) {
          // 创建 error 组件的构造函数,并保存在 errorComp 属性中
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        // 处理 高级异步组件 中的 loading
        if (isDef(res.loading)) {
          // 创建 loading 组件的构造函数,并保存在 loadingComp 属性中
          factory.loadingComp = ensureCtor(res.loading, baseCtor)
          if (res.delay === 0) {
            // 如果 delay 为 0 的话,说明要立即进行加载中的状态
            factory.loading = true
          } else {
            // 如果 delay 不等于 0 的话,则需要 delay 之后再进行 loading 的处理
            // 此处使用 setTimeout(() => {}, res.delay || 200)
            setTimeout(() => {
              // delay 毫秒之后,如果不是 resolved 和 error 的状态的话,说明当前是 loading 状态
              if (isUndef(factory.resolved) && isUndef(factory.error)) {
                // 将加载中的标志为 true,然后重新渲染视图,渲染出加载组件
                factory.loading = true
                forceRender()
              }
            }, res.delay || 200)
          }
        }

        // 处理 高级异步组件 中的 timeout
        // timeout 参数表示:timeout 毫秒之后,如果这一个异步组件还不是 resolved 状态的话,
        // 就将组件设为 error 状态,并重新渲染,渲染出 error 组件,借助 setTimeout 和 reject 方法实现功能
        if (isDef(res.timeout)) {
          setTimeout(() => {
            if (isUndef(factory.resolved)) {
              reject(
                process.env.NODE_ENV !== 'production'
                  ? `timeout (${res.timeout}ms)`
                  : null
              )
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    // 如果 factory.loading 为 true 的话,说明异步组件还在加载中,此时返回 loadingComp
    // 如果不为 true 的话,说明异步组件加载完成,返回 resolved 异步组件即可
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

标签:异步,Vue,res,factory,34,源码,error,组件,加载
来源: https://blog.csdn.net/f18855666661/article/details/120392236

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有