乐趣区

关于vue.js:Vue-异步组件的实现原理

异步组件

为什么须要异步组件

Vue 作为单页面利用在加载首页时常会遇到加载迟缓的问题,导致在应用体验较差,这是因为在打包单页面利用时,页面会吧脚本打包成一个文件,这个文件蕴含了所有业务和非业务的代码,脚本文件过大导致渲染页面时迟缓。
在进行首屏性能优化时,最罕用的办法就是对于文件的拆分和代理的拆散,按需加载的概念也是在这个前提下引入的。因而在 Vue 开发过程中,咱们会把一些非首屏的组件设计成异部组件,局部不影响首次视觉体验的组件也能够设计成异步组件。

定义异步组件

Vue 中,在注册组件时应用一个工厂函数定义组件,这个工厂函数会异步解析组件定义,只有当这个组件须要被渲染时才会触发该工厂函数,并且会把后果缓存起来以便后续应用。

// 注册全局组件时,定义为异步组件
Vue.component("async-component", (resolve, reject)=>{setTimeout(()=>{
    // 应用一个定时器来模仿异步加载过程
    // resolve 须要返回一个组件的定义对象,该对象的属性与定义一般组件的属性统一
    resolve({template: "<div>async-component</div>"})
  }, 2000)
})

// 注册部分异步组件
const vm = new Vue({
  components:{asyncComponent: ()=> import('./test.vue')
  }
})

异步组件流程剖析

在组件根底的剖析过程中,咱们剖析了实例的挂载流程分为依据渲染函数创立 Vnode 和依据 Vnode 创立实在 DOM 的过程。在创立 Vnode 的过程中,如果遇到组件占位符,会调用 createComponent,在该办法中,会为子组件做选项合并和装置钩子函数。异步组件的解决也是在该函数中进行的。

function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {if (isUndef(Ctor)) {return}

  /**
   * 这里的 baseCtor 为 Vue 构造函数
   */
  const baseCtor = context.$options._base

  // async component
  // 异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // 合并结构器配置
  resolveConstructorOptions(Ctor)



  // 装置组件钩子函数
  installComponentHooks(data)

  const name = Ctor.options.name || tag
  const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    {Ctor, propsData, listeners, tag, children},
    asyncFactory
  )

  return vnode
}

在注册异步组件时,Vue.component(name, options) 第二个参数工厂函数不是一个一般对象,因而无论是全局注册还是部分注册,都不会执行 Vue.extend 办法生成子类结构器,所以在下面的 createComponent 办法中,Ctor.cid 不会存在,代码进入异步组件的分支。

异步组件的外围是 resolveAsyncComponent 函数,咱们次要关怀工厂函数的解决局部,来看下精简后的代码

function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {if (owner && !isDef(factory.owners)) {const owners = factory.owners = [owner]
    let sync = true
    let timerLoading = null
    let timerTimeout = null

    ;(owner: any).$on('hook:destroyed', () => remove(owners, owner))

    

    const resolve = once((res: Object | Class<Component>) => {})

    const reject = once(reason => {})

    const res = factory(resolve, reject)

    // return in case resolved synchronously
    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}

依据下面的代码,针对异步组件工厂函数的写法,咱们能够总结成三个步骤:

  • 定义异步申请胜利和申请失败的处理函数
  • 执行组件定义的工厂函数
  • 同步返回申请胜利的函数解决

resolvereject 函数都是 once 办法执行的后果,once 办法的作用是避免屡次调用异步组件,使得 resolvereject 只会执行一次。

function once (fn: Function): Function {
  // 利用闭包的个性将 called 作为标记位
  let called = false
  return function () {if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

来看下 resolvereject 的解决逻辑

const forceRender = (renderCompleted: boolean) => {for (let i = 0, l = owners.length; i < l; i++) {(owners[i]: any).$forceUpdate()}
  if (renderCompleted) {
    owners.length = 0
    if (timerLoading !== null) {clearTimeout(timerLoading)
      timerLoading = null
    }
    if (timerTimeout !== null) {clearTimeout(timerTimeout)
      timerTimeout = null
    }
  }
}

const resolve = once((res: Object | Class<Component>) => {
  // 专程组件结构器,并缓存到 resolved 属性中
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    // 强制刷新视图
    forceRender(true)
  } else {owners.length = 0}
})

// 组件加载失败处理函数
const reject = once(reason => {
  process.env.NODE_ENV !== 'production' && warn(`Failed to resolve async component: ${String(factory)}` +
    (reason ? `\nReason: ${reason}` : '')
  )
  if (isDef(factory.errorComp)) {
    factory.error = true
    forceRender(true)
  }
})

组件结构器创立实现之后,会进行一次视图的从新渲染。 因为 Vue 是数据驱动试图进行渲染的,则组件在加载结束之后,并没有产生数据的变动,因而须要手动强制更新试图 forceRender 函数外部会拿到每个调用异步组件的实例,而后执行 Vue 原型上的 $forceUpdate 办法刷新视图。在异步组件加载过程中,因为 Ctorundefine 会同步创立一个正文节点,在异步组件加载实现之后,触发 $forceUpdate 再次执行 createEmptyVNode,这是 Ctor 不为 undefined,因而会走失常的组件渲染流程

Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
  // return a placeholder node for async component, which is rendered
  // as a comment node but preserves all the raw information for the node.
  // the information will be used for async server-rendering and hydration.
  return createAsyncPlaceholder(
    asyncFactory,
    data,
    context,
    children,
    tag
  )
}

Promise 异步组件

异步组件的另一种写法是在工厂函数中返回一个 Promise 对象,在 ES6 中引入了 import 来加载模块,import 是一个运行时加载模块的办法,,能够和 require 进行比照,import 是异步加载的,require 是同步加载的,并且 import 会返回一个 Promise 对象

具体用法

Vue.component("asyncComponent", ()=> import('./test.vue'))

分明 Promise 异步组件的注册形式之后,持续来剖析异步的流程。

const res = factory(resolve, reject)

if (isObject(res)) {if (isPromise(res)) {if (isUndef(factory.resolved)) {res.then(resolve, reject)
    }
  }
}

在工厂函数外部应用 import 引入一个异步组件时,工厂函数回返回一个 Promise 对象,胜利加载则执行 resolve,失败则执行 reject。其中判断一个对象是否为 Promise 对象最简略的办法就是是否存在 thencatch 办法

function isPromise (val: any): boolean {
  return (isDef(val) &&
    typeof val.then === 'function' &&
    typeof val.catch === 'function'
  )
}

高级异步组件

为了在应用时能有更好的体验,能够在加载异步组件的过程中应用 loading 组件来显示一个期待状态,应用 error 组件解决组件加载失败的谬误提醒等。在定义异步组件时,工厂函数能够返回一个对象,对象中能够定义须要加载的组件 component,加载过程中显示的 loading 组件,加载谬误显示的 error 组件。在组件渲染过程中,同样进入异步组件的分支

Vue.component("asyncComponent", ()=> {
  // 须要加载的组件
  component: import('./test.vue'),
  // 加载过程中显示的 loading 组件
  loading: LoadingComponent,
  // 组件加载谬误时显示的组件
  error: ErrorComponent,
  // loading 组件显示的延迟时间,默认为 200,如果 delay 工夫后,组件还没有加载胜利,则显示 loading 组件
  delay: 200,
  // 组件加载超时工夫,超过该工夫组件为加载胜利,认为组件加载失败,应用 error 组件进行提醒
  timeout: 3000
})

对于高级异步组件,工厂函数返回的是一个对象,来看下对于高级异步组件 Vue 的处理过程

if (isObject(res)) {if (isPromise(res)) {// 工厂函数返回一个 Promise 的解决逻辑} else if (isPromise(res.component)) {
    // 高级异步组件的解决流程

    // 组件加载过程了 Promise 异步组件解决形式雷同
    res.component.then(resolve, reject)

    if (isDef(res.error)) {
      // 定义了谬误组件时,创立谬误组件的子类结构器,并保留到 errorComp 中
      factory.errorComp = ensureCtor(res.error, baseCtor)
    }

    if (isDef(res.loading)) {
      // 创立加载时组件子类结构器,并保留到 loadingComp zhong
      factory.loadingComp = ensureCtor(res.loading, baseCtor)
      if (res.delay === 0) {
        // 立刻展示加载时组件
        factory.loading = true
      } else {timerLoading = setTimeout(() => {
          timerLoading = null
          // 延时指定工夫后,组件还没加载实现并没有加载失败,显示加载时组件
          if (isUndef(factory.resolved) && isUndef(factory.error)) {
            factory.loading = true
            forceRender(false)
          }
          // 默认显示加载时组件延时工夫为 200 ms
        }, res.delay || 200)
      }
    }

    if (isDef(res.timeout)) {timerTimeout = setTimeout(() => {
        timerTimeout = null
        if (isUndef(factory.resolved)) {
          // 规定工夫内异步组件没有加载胜利,触发加载失败
          reject(
            process.env.NODE_ENV !== 'production'
              ? `timeout (${res.timeout}ms)`
              : null
          )
        }
      }, res.timeout)
    }
  }
}

通过剖析上方代码能够看出,高级组件的加载过程 Promise 组件的加载过程雷同,额定增加了加载失败和加载过程中的解决逻辑。通过 delay 属性来提早显示加载中组件的显示,通过 timeout 属性来规定超时工夫。

webpack 异步组件用法

在应用 webpack 打包 Vue 利用时,咱们能够将异步组件的代码进行拆散。webpack 为异步组件的加载提供了两种写法

  • require.ensure(): 这是 webpack 提供给异步组件的写法,webpack 在打包时,会动态地解析代码中的 require.ensure(),同时将模块增加到一个离开的 chunk,中,其中函数的第三个参数为拆散代码块的名称
Vue.component("asyncComponent", (resolve, reject)=>{require.ensure([], ()=>{resolve(require('./test.vue'))
  }, "asyncComponent") // 这里的 asyncComponent 为 chunkName
})
  • import(/* webpackChunkName: "chunkName" */, component):ES6import 办法是举荐的写法,通过正文 webpackChunkName 来指定拆散后组件模块的命名
Vue.component('asyncComponent', () => import(/* webpackChunkName: "asyncComponent" */, './test.vue'))
退出移动版