异步组件

讲完组件,那么正好下面来看看异步组件。异步组件在项目开发中很重要。结合webpackcode-spliting就能让组件打包成的js异步加载,达到浏览器加载优化的目的,提高页面渲染速度。 因为是配合webpack使用,所以我们需要用vue-cli创建一个最基本的项目,那么好首先我们看这么一个例子

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

new Vue({
  render: h => h(App)
}).$mount('#app')
1
2
3
4
5
6
7
8
9
10

我们把demoApp组件的局部注册删除,然后全局注册,上面是例子代码。

例子准备好了,接下来我们要想一想入口在哪里。打开package-json我们看到,它是通过vue-cli-service启动的,显然我们要找到启动文件。 在node_modules中,我们看.bin目录,在.bin目录下我们能找到vue-cli-service脚本文件。在这里我们能看到他的执行目录是@vue/cli-service/bin/vue-cli-service.js。 找的过程就不细描述了,我们直接看config/base.js里的代码

webpackConfig.resolve
  .alias
    .set(
      'vue#39;,
      options.runtimeCompiler
        ? 'vue/dist/vue.esm.js'
        : 'vue/dist/vue.runtime.esm.js'
    )
1
2
3
4
5
6
7
8

很简单如果我们开启了runtimeCompiler选项引入的就是vue.esm.js,否则就是vue.runtime.esm.js。两者差别就是是否存在compiler了。

引入的vue源码知道在哪里了,现在我们想想异步组件的入口在哪里。首先在上一篇文章中我们知道,创建异步组件会走createComponent方法,这个方法在vdom/create-component文件中, 显然现在我们只要在createComponent的开头写上debugger就能在开发状态进入调试模式了

初始化

在这里我们全局注册的时候,第二个参数是一个方法而不是对象,所以我们在执行initAssetRegisters方法的时候,当type=component的时候

if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id
  definition = this.options._base.extend(definition)
}
1
2
3
4

看上面isPlainObjectfalse,那么vue就不会执行里面的代码。然后我们再看createComponent方法。

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

  // 异步组件
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    // 组件构造工厂函数  Vue
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }
  return vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

可以看到 Ctor我们这里传入的是function,且是没有执行过extend的。所以它不存在cid,也就是进入了这里面的语句。入口分析完了,那么我们看里面的方法 首先是resolveAsyncComponent

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) => {}

    const resolve = once((res: Object | Class<Component>) => {})
    // 用回调函数的方式
    const reject = once(reason => { })

    const res = factory(resolve, reject)

    sync = false

    return factory.loading
      ? factory.loadingComp
      : factory.resolved
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

currentRenderingInstance获取当前渲染实例,也就是App的实例。该方法较长,我们先删除该例子下 现阶段无关的代码只看核心,第一次使用factory.owners可定是不存在的。所以会进入if

在里面我们首先声明了很多属性,主要就是声明了resolvereject函数,并且他们只执行一次。当执行到factory(resolve, reject)的时候,其实们是执行了定义函数,也就是说我们要执行

require(['./components/HelloWorld.vue'], resolve)
1

requirewebpack的方法,因此我们将进入webpack进行执行。通过上面的resolvereject函数我们可以猜测,require肯定是new了一个Promise

确实如此,在webpack里的代码,就是new Promise,然后动态创建一个script后。然后回到之后的流程继续执行。在一大段的if语句判断中,其实现在是不执行的因为没有返回值,当前的res是空。所以当前就是执行了后两段代码。sync = false并且返回factory.resolved

然而当前factory.resolvedundefined,所以看createComponent方法

if (Ctor === undefined) {
  return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
1
2
3

也就是进入createAsyncPlaceholder,看名字是创建了一个异步的Placeholder。我们看代码

export function createAsyncPlaceholder (
  factory: Function,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag: ?string
): VNode {
  const node = createEmptyVNode()
  node.asyncFactory = factory
  node.asyncMeta = { data, context, children, tag }
  return node
}
1
2
3
4
5
6
7
8
9
10
11
12

很简单,vue创建了一个空的vnode,然后吧参数赋值给了vnode。当执行到insert节点之后我们能看到 console的元素页面内是

<div id="app">
  <img alt="Vue logo" src="/img/logo.82b9c7a5.png">
  <!---->
  </div>
1
2
3
4

看在组件区域存在一注释节点。并且在network我们可以看到一个空的0.js。这样初始化流程结束

执行 resolve

这里我们在resolve函数内部打个断点,然后看调用堆栈。上面有个大大的Promise.then异步,然后就会执行到 resolve函数。

const resolve = once((res: Object | Class<Component>) => {
  factory.resolved = ensureCtor(res, baseCtor)
  if (!sync) {
    forceRender(true)
  } else {
    owners.length = 0
  }
})
function ensureCtor (comp: any, base) {
  if (
    comp.__esModule ||
    (hasSymbol && comp[Symbol.toStringTag] === 'Module')
  ) {
    comp = comp.default
  }
  return isObject(comp)
    ? base.extend(comp)
    : comp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这个方法很简单

  1. 执行ensureCtor,该方法就是通过拿到的组件对象执行Vue.extend初始化子组件构造函数。
  2. 当前syncfalse。所以我们会执行forceRender(true)

forceRender很简单,我们看代码

var forceRender = function (renderCompleted) {
   for (var i = 0, l = owners.length; i < l; i++) {
     owners[i].$forceUpdate()
   }
}
1
2
3
4
5

这里我们拿到闭包保存的owners[i],当前只有一个也就是App实例。那么后面就是this.$forceUpdate的执行了。重新运行watcher.update进行页面更新。这时候我们可以拿到子组件实例了,也就是进行正常的createComponent流程,渲染到页面上

其他例子

一般来说我们不会使用上面的方法,他有更好基于es2015的书写方式

Vue.component('HelloWorld',
  // 该 import 函数返回一个 promise 对象
  () => import(/* webpackChunkName: "HelloWorld" */'./components/HelloWorld.vue')
)
1
2
3
4

在这个例子中需要注意的是 我们有返回值,这个返回值是webpack处理后返回的,就是Promise。既然有返回值,那么在下面代码中

 const res = factory(resolve, reject)
 // 如果是一个promise
 if (isObject(res)) {
   if (isPromise(res)) {
     // () => Promise
     if (isUndef(factory.resolved)) {
       res.then(resolve, reject)
     }
   }
 }

1
2
3
4
5
6
7
8
9
10
11

显然我们会运行到res.then方法,也就会执行到我们定义的resolve,之后的代码是一样的。

在官方文档中还有一个例子

// 第三种写法 高级异步组件
const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./components/HelloWorld.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})

const LoadingComp = {
  template: '<div>loading</div>'
}
const ErrorComp = {
  template: '<div>error</div>'
}
Vue.component('HelloWorld', AsyncComp)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个语法是新增的,代码的执行其实很简单就是判断promise的执行情况。在源码中也很简单,相信都能看明白。

总结

如果这时候有面试官问:vue的异步组件是如何执行的?

就能回答异步组件和名字一样,其实就是通过webpack创建的promise等执行到then的时候去初始化子组件构造函数,然后在执行当前实例也就是父组件实例的$foreUpdate去重新渲染。

Last Updated: