解密vue异步组件

345 阅读3分钟

1、异步组件三种用法

//方式1
Vue.component('async-component', function (resolve, reject) {
   require(['./MyAsyncComponent'], resolve);
});


//方式2
Vue.component(  
    'async-webpack-example',   
    () => import('./my-async-component')  
)

//方式3
Vue.component('async-example', () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
}))

2、使用场景

如果我们需要展示某个产品的信息,产品关联的tab页面有20个,使用异步组件就使得代码很简单,减少了组件的注册

<template> 
   <el-tabs v-model="activeName" @tab-click="handleClick"> 
       <el-tab-pane label="基本信息" name="first"></el-tab-pane> 
       <el-tab-pane label="配置信息" name="second"></el-tab-pane> 
       <el-tab-pane label="角色信息" name="third"></el-tab-pane> 
       <el-tab-pane label="部门信息" name="fourth"></el-tab-pane> 
       ...
       <el-tab-pane label="其他信息" name="twenty"></el-tab-pane>
    </el-tabs> 
    
    <async-webpack-example />
</template> 
<script> 
  export default { 
      data() { return { activeName: 'second' }; }, 
      methods: { 
        handleClick(path) { 
           Vue.component(  
                'async-webpack-example',   
                () => import('./my-async-component/'+ path)  
            )
        } 
      } 
   }; 
 </script>


3、源码

src\core\vdom\create-component.ts

3.1 createComponent

  1. Ctor.cid为空,则为异步组件,使用 resolveAsyncComponent 加载。
  2. 创建一个占位符组件,在异步组件加载完成前用于渲染。
export function createComponent(
  Ctor: typeof Component | Function | ComponentOptions | void,
  data: VNodeData | undefined,
  context: Component,
  children?: Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base

  // 由于我们这个时候传入的 `Ctor` 是一个函数,那么它也并不会执行 `Vue.extend` 逻辑,因此它的 `cid` 是 `undefiend`,进入了异步组件创建的逻辑
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor as typeof Component)
  }

  if (typeof Ctor !== 'function') {
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
     // 1、异步组件加载
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // 2、创建一个占位符
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
  }

  data = data || {}


  resolveConstructorOptions(Ctor as typeof Component)

  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // 函数组件
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(
      Ctor as typeof Component,
      propsData,
      data,
      context,
      children
    )
  }

  const listeners = data.on
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    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 = getComponentName(Ctor.options) || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data,
    undefined,
    undefined,
    undefined,
    context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

3.2 resolveAsyncComponent

src\core\vdom\helpers\resolve-async-component.ts

  1. 执行 factory 方法
  2. 异步组件使用方式2、3返回一个promise对象,import后执行 forceRender->forceUpdate
function ensureCtor(comp: any, base) {
  if (comp.__esModule || (hasSymbol && comp[Symbol.toStringTag] === 'Module')) {
    comp = comp.default
  }
  return isObject(comp) ? base.extend(comp) : comp
}


export function createAsyncPlaceholder(
  factory: Function,
  data: VNodeData | undefined,
  context: Component,
  children: Array<VNode> | undefined,
  tag?: string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
export function resolveAsyncComponent(
  factory: { (...args: any[]): any; [keye: string]: any },
  baseCtor: typeof Component
): typeof Component | void {
  if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
  }

 // ----第二次进来不为空----
  if (isDef(factory.resolved)) {  
    return factory.resolved
  }

  const owner = currentRenderingInstance
  if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner)
  }

  if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
  }

  if (owner && !isDef(factory.owners)) {
    const owners = (factory.owners = [owner])
    let sync = true
    let timerLoading: number | null = null
    let timerTimeout: number | null = null

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

    const forceRender = (renderCompleted: boolean) => {
      for (let i = 0, l = owners.length; i < l; i++) {
        owners[i].$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 | Component) => {
      factory.resolved = ensureCtor(res, baseCtor) //----factory.resolved---
      if (!sync) {
        forceRender(true)
      } else {
        owners.length = 0
      }
    })

    const reject = once(reason => {
      if (isDef(factory.errorComp)) {
        factory.error = true
        forceRender(true)
      }
    })

    const res = factory(resolve, reject)

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

        if (isDef(res.error)) {
          factory.errorComp = ensureCtor(res.error, baseCtor)
        }

        if (isDef(res.loading)) {
          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)
              }
            }, res.delay || 200)
          }
        }

        if (isDef(res.timeout)) {
          timerTimeout = setTimeout(() => {
            timerTimeout = null
            if (isUndef(factory.resolved)) {
              reject(null)
            }
          }, res.timeout)
        }
      }
    }

    sync = false
    return factory.loading ? factory.loadingComp : factory.resolved
  }
}

3.3 forceRender

当执行 forceRender forceUpdate 的时候, 会触发组件的重新渲染,见 vm._update(vm.render())那么会再一次执行 resolveAsyncComponent,这时候就会根据不同的情况,可能返回 loading、error 或成功加载的异步组件,返回值不为 undefined,因此就走正常的组件 renderpatch 过程,与组件第一次渲染流程不一样,这个时候是存在新旧 vnode 的

src\core\instance\lifecycle.ts

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

4、疑问

疑问:webpack编译是按需加载,也就是我们用到了哪些代码,才会进行编译打包。当我们使用异步组件的时候,path是一个变量,如何做到把这些路径的文件提前编译呢?

带着这样的疑问我们来先看看 如下的demo效果吧

// 静态导入
import  './my-async-component/a'


// 动态导入
import(path) 
import('./my-async-component/'+ path) 

我们写个demo,可以看出

  • import('./com/'+ path)
    是将'./com/'下所有的文件提前编译了,文件src/dd.js就没有被编译

image.png

  • import(path) webpack entry:'./src/index.js', 是将src 下所有的文件提前编译了,文件src/dd.js就被编译了 image.png

  • import(path) webpack entry:'./index.js', 是将整个项目所有的文件提前编译了,包括 node_modules

image.png

通过上面我们可以看出,webpack import 动态导入的时候,会根据 路径 进行提前编译,当我们路径是变量的时候,会根据不同的情况编译不同内容,为了防止非必要的文件被提前编译,我们的路径尽可能要写的明确一点

5、webpack 打包 import动态编译源码

想要了解去另外一篇 webpack编译require、import

欢迎关注我的前端自检清单,我和你一起成长