Vue源码学习(十二)异步组件

本文主要讲述vue中的异步组件,如果你看完本文相信你应该对vue中的异步组件有着相对深刻的理解,
如果你还不知道vue中异步组件是什么,请参考vue异步组件

为什么要异步组件

异步组件是vue性能优化的一种方式,主要目的是为了解决页面引入很多组件导致打包后的js体积过大,我们知道同样条件下,文件体积越大,请求耗时越长,因此
vue提供了异步组件,当页面中通过异步方式来声明(全局)或者注册(局部)组件时,每个异步组件会被单独打包成一个js文件,每个异步组件对应的js文件在需要时才会被加载。
通过减小打包文件体积的方式实现了性能优化。

组件按需加载通常使用在路由中,但页面上其他组件也可以通过异步组件实现懒加载
App.vue





Foo.vue




Bar.vue





Bar组件会被单独打包成0.bundle.js,页面初始化加载不会请求js文件,当点击查看bar组件时,会请求0.bundle.js,并在页面上渲染Bar组件。
这里提供了一种思路从而实现了非路由组件的按需加载。那么路由组件的按需加载使用起来就比较简单,路由组件直接使用异步组件方式引用即可。

异步组件和普通组件的区别声明

普通组件无论时全局组件还是局部组件都是一个对象,而异步组件定义为一个函数或者一个Promise。
异步组件声明有如下三种方式,函数式和Promise以及高级异步组件
函数式

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})

import语法
webpack import语法会返回一个Promise

{
	...
	components: {
		Bar: () => import('./Bar.vue')
	}
	...
	
}

高级异步组件

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

局部异步组件建议使用import语法,相对函数式声明更加简洁

源码分析

下面分别从源码角度分析函数式异步,import语法和高级异步组件,主要涉及到createComponent和resolveAsyncComponent两个函数

函数式

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

  const baseCtor = context.$options._base

  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // 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 || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
export function resolveAsyncComponent (
  factory: Function,
  baseCtor: Class<Component>
): Class<Component> | void {
		const owner = currentRenderingInstance
		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 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>) => {
		    // cache resolved
		    factory.resolved = ensureCtor(res, baseCtor)
		    // invoke callbacks only if this is not a synchronous resolve
		    // (async resolves are shimmed as synchronous during SSR)
		    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)
		    }
		  })
		
		  const res = factory(resolve, reject)
			sync = false
			// return in case resolved synchronously
			return factory.loading
			  ? factory.loadingComp
			  : factory.resolved
}

createComponent创建组件vnode,如果是异步组件,那么Ctor.cid为undefined,会执行Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
在resolveAsyncComponent方法中,会将当前组件或根实例添加到owners中,也就是异步组件的拥有者。
调用once方法包装异步组件声明中的resolve和reject函数,然后执行factory函数,此时会加载组件也就是执行realComp = require('./my-async-component'),
realComp(伪代码)为真实组件
请求文件是一个异步过程,此时sync为false,文件请求成功后会执行resolve函数,调用ensureCtor方法将realComp转化我组件构造器。此时sync为false,会调用forceRender方法
forceRender中会遍历owers,调用根实例或组件实例的forceUpdate方法,此时会进入新一轮的渲染,在执行到createComponent方法时,会再次执行resolveAsyncComponent,返回
factory.resolved也就是ensureCtor方法生成的组件构造器,此时会创建真正的组件vnode.最终渲染后的dom会取代之前的注释节点。

import语法

const res = factory(resolve, reject)
if (isObject(res)) {
      if (isPromise(res)) {
        // () => Promise
        if (isUndef(factory.resolved)) {
          res.then(resolve, reject)
        }
      } 
    }

执行factory函数也就是() => import('./Foo.vue')返回的res为Promise,isPromise(res)为true但factory.resolved为undefined,所以此时会执行resolve函数,从而执行forceRender,后续步骤和
函数声明的异步组件相同。

高级异步组件

高级异步组件只是在组件的渲染过程中增加了loading,error,和comp,loading对应加载中组件,error为渲染失败时展示组件,如果定义了loading属性,并且delay不为0,那么高级异步组件整个成功的渲染过程不会
被渲染成注释节点,而是LoadingComp=>TargetCompent,即Loading组件和真正渲染的组件MyComponent

总结

非高级异步组件渲染过程: 渲染成注释节点,请求到组件后会调用forceUpdate方法强制根实例或者组件实例更新,此时factory.resolved为Ctor,第二轮更新的过程和普通组件相同

高级异步组件只是功能相对丰富,函数返回的对象具有loading和error属性,增加了异步组件的功能,方便展示请求中和请求失败的效果。其本质和普通异步组件相同,不过是增加了几个分支。

你可能感兴趣的:(vue,vue源码笔记)