异步组件三种方式
- 普通函数异步组件
1 | Vue.component('aa', function(resolve, reject) { |
- Promise 异步组件
1 | Vue.component('aa', () => import('./aa.js') ) |
- 高级异步组件
1 | const aa = () => ({ |
从以上示例中可以看到,通过Vue.component
注册的组件不再是一个对象,而是一个函数,这个函数也不是组件构造函数,是一个工厂函数。这个工厂函数有两个参数resolve
函数和reject
函数,其是 Vue 内部定义的,在这个工厂函数中有个异步函数,当异步函数执行成功后调用resolve
函数,其参数就是异步组件的对象。
源码分析
组件的使用,要先在vm._render
过程中执行vnode = createComponent(Ctor, data, context, children, tag)
生成vnode
,其中参数Ctor
可以是函数或对象,从createComponent
方法开始介绍异步组件是如何使用。
createComponent
1 | function createComponent(Ctor, data, context, children, tag) { |
当参数Ctor
值的类型是函数时,不会执行Ctor = baseCtor.extend(Ctor)
。
因为在 Vue 中是调用Vue.extend
方法来创建继承 Vue 的组件构造函数。在Vue.extend
中会执行Sub.cid = cid++
给组件构造函数的cid
属性赋值。
1 | var cid = 1 |
所以可以用isUndef(Ctor.cid)
来判断Ctor
是不是一个组件构造函数,若不是执行Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
进入异步组件使用的逻辑。
resolveAsyncComponent
1 | function resolveAsyncComponent(factory, baseCtor) { |
resolveAsyncComponent
函数,是个高阶函数,主要对注册异步组件时,传入不同的工厂函数进行处理,内部定义了工厂函数的参数resolve
函数和reject
函数,并调用了工厂函数,成功执行resolve
函数,失败执行reject
函数,最后返回组件构造函数或 undefined。
异步组件和同步组件的注册原理是一样,只是异步组件的使用原理跟同步组件是不一样的。
异步组件的使用原理
1. 普通函数异步组件
1 | Vue.component('aa', function(resolve, reject) { |
resolveAsyncComponent(factory, baseCtor)
,参数factory
的值就是上面Vue.component
的第二参数,参数baseCtor
是 Vue 构造函数。
1 | function resolveAsyncComponent(factory, baseCtor) { |
在 resolveAsyncComponent
函数中,内部定义了三个函数 forceRender
、resolve
、reject
。其中 resolve
和 reject
函数是用 once
函数包装。
1 | function once(fn) { |
once
函数是个高阶函数,巧妙利用闭包和 called
变量,保证所包装的函数只执行一次。也就是确保 resolve
和 reject
函数只执行一次。
因为在 resolveAsyncComponent
函数中最后执行 return factory.loading ? factory.loadingComp : factory.resolved
,返回 factory.resolved
。
factory.resolved
定义在resolve
函数中。
1 | var resolve = once(function(res) { |
执行 ensureCtor(res, baseCtor)
后赋值给 factory.resolved
,来看一下 ensureCtor
方法。
1 | function ensureCtor(comp, base) { |
参数base
为 Vue 构造函数,那么最后执行return isObject(comp) ? base.extend(comp) : comp
,如果参数comp
是个对象,执行base.extend(comp)
,也就是执行Vue.extend(comp)
生成一个继承 Vue 的构造函数。
参数comp
是通过resolve
函数的参数res
传参的。回到resolveAsyncComponent
方法中,有执行var res = factory(resolve, reject)
这段代码,factory
是通过Vue.component
的第二参数传参的,值如下所示。
1 | function(resolve, reject) { |
其中resolve
就是resolveAsyncComponent
函数内部定义的resolve
函数。那么参数comp
的值如下所示,是个组件的选项对象。
1 | { |
这样执行ensureCtor(res, baseCtor)
会得到一个组件构造函数,那么factory.resolved
的值为一个组件构造函数。
再回到createComponent
方法中,看这段代码
1 | Ctor = resolveAsyncComponent(asyncFactory, baseCtor) |
执行resolveAsyncComponent
方法后返回一个组件构造函数赋值给Ctor
。就这么结束了吗。当然不是了,不知你有没有注意到在Vue.component
定义的第二参数中,resolve(//...)
外层还有一个setTimeout
定时器,是个异步任务。JavaScript 是单线程的,异步任务要等所有同步任务都执行完才能执行。故此时resolveAsyncComponent
方法中的resolve
函数是不执行,factory.resolved
应该为 undefined 。那么Ctor
为 undefined ,要执行return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
。createAsyncPlaceholder
方法是用来创建一个注释节点vnode
作为占位符。
1 | function createAsyncPlaceholder(factory, data, context, children, tag) { |
此时createComponent
方法生成的一个注释节点vnode
,而不是一个组件vnode
,那组件要怎么渲染,不着急,再回到resolveAsyncComponent
方法中,在 return 之前执行sync = false
,在1000ms后resolve
函数执行,会执行forceRender(true)
,来看一下forceRender
函数。
1 | var resolve = once(function(res) { |
currentRenderingInstance
是使用异步组件的当前 Vue 实例,赋值给owner
。
如果同一个异步组件在很多个地方局部注册。这样要重复执行很多次相同的resolve
函数。所以在这里做了个优化。
异步组件是以一个工厂函数factory
来定义组件,在factory
定义一个属性owners
,来存储使用异步组件的当前 Vue 实例,也就是调用factory
函数的上下文环境。
若owner
有值和factory.owners
不存在,则说明factory
函数是第一次执行。若owner
有值和factory.owners
有值,则说明factory
函数已经执行过了。执行factory.owners.indexOf(owner) === -1
判断factory.owners
中有没有当前 Vue 实例,若没有,则把当前 Vue 实例添加到factory.owners
中。
回到forceRender
函数中,执行(owners[i]).$forceUpdate()
相当执行vm.$forceUpdate()
这个实例方法。这是因为异步组件加载过程中是没有数据发生变化的,所以要通过执行vm.$forceUpdate()
迫使 Vue 实例重新渲染一次。
1 | Vue.prototype.$forceUpdate = function() { |
执行vm._watcher.update()
相当执行mountComponent
方法中的vm._update(vm._render(), hydrating)
,在执行vm._render()
过程中调用createComponent
方法又执行到以下逻辑。
1 | // async component |
再次执行resolveAsyncComponent(asyncFactory, baseCtor)
时,1000ms已过,故异步组件注册的工厂函数factory
中的resolve
函数已经执行完毕,故factory.resolved
有值,直接返回factory.resolved
。
1 | function resolveAsyncComponent(factory, baseCtor) { |
2. Promise 异步组件
1 | Vue.component('aa', () => import('./aa.js') ) |
resolveAsyncComponent(factory, baseCtor)
,参数baseCtor
是 Vue 构造函数。参数factory
的值就是上面Vue.component
的第二参数,返回值是import('./aa.js')
,它是一个 Promise 对象。整理一下代码,跟此场景无关的代码都去掉。
1 | function resolveAsyncComponent(factory, baseCtor) { |
因为在此场景中工厂函数factory
的返回值是一个 Promise 对象,所以满足isObject(res)
和isPromise(res)
的条件,执行以下逻辑
1 | if (isUndef(factory.resolved)) { |
因为返回的是 Promise 对象,其实例方法then
的参数是两个的函数resolve
和reject
。当执行成功会执行resolve
函数,当执行失败会调用reject
函数。
所以这里巧妙地执行res.then(resolve, reject)
,当执行成功后会掉resolve
函数,而这个resolve
函数,是resolveAsyncComponent
函数中自定义的。接下来的逻辑就和普通函数异步组件一模一样。
3. 高级异步组件
在高级异步组件中,可定义异步组件加载中展示的组件和加载失败展示的组件,对用户更友好。
1 | const aa = () => ({ |
resolveAsyncComponent(factory, baseCtor)
,参数baseCtor
是 Vue 构造函数。参数factory
的值就是上面Vue.component
的第二参数,返回值是一个对象。整理一下代码,跟此场景无关的代码都去掉。
1 | function resolveAsyncComponent(factory, baseCtor) { |
因为在此场景中工厂函数factory
的返回值是一个对象res
,
若其中res.component
属性是一个 Promise 对象,执行res.component.then(resolve, reject)
。
若res.error
有值,执行factory.errorComp = ensureCtor(res.error, baseCtor)
,把加载失败展示的组件转换成组件构造函数赋值给factory.errorComp
。
若res.loading
有值,执行factory.loadingComp = ensureCtor(res.loading, baseCtor)
,把加载中展示的组件转换成组件构造函数赋值给factory.loadingComp
。
若res.delay
的值为 0 ,则说明要直接展示加载中的组件,把factory.loading
设置为 true 。
若res.delay
的值不为 0 ,则说明要经过一段delay
时间的延迟才展示加载中的组件,用 setTimeout 定时器在经过一段delay
时间的延迟,在异步组件没有加载成功或者失败的情况下把factory.loading
设置为 true ,并执行forceRender(false)
,触发组件更新的 patch 过程渲染出加载中展示的组件。
若res.timeout
有值,用 setTimeout 定时器在在超出res.timeout
后异步组件还未加载完成,报错一个超时的错误。
最后如果加载中组件构造函数factory.loading
有值返回factory.loading
,就不必调用createAsyncPlaceholder
方法创建注释节点来作为占位节点,直接用加载中展示的组件生成的 DOM 节点来作为占位节点。
异步组件是在组件更新的 patch 过程才渲染的,会再调用resolveAsyncComponent
方法。
1 | if (isTrue(factory.error) && isDef(factory.errorComp)) { |
若factory.error
为true且加载失败组件构造函数factory.errorComp
存在,返回factory.errorComp
。
若factory.loading
为true且加载中组件构造函数factory.loadingComp
存在,返回factory.loadingComp
。
异步组件加载成功返回factory.resolved
,接下来的逻辑就和普通函数异步组件一模一样。
最后在介绍一下异步组件加载失败时处理,其会调用自定义的reject
函数,若factory.errorComp
,把factory.error
置为true。然后执行forceRender(true)
,此时其参数为true,在强制重新渲染中可以把加载中和加载超时中的定时器清空。
总结
Vue 的异步组件有 3 种实现方式,其中高级异步组件实现了loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了delay
为 0 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步加载组件成功后,执行resolve
函数,在其中调用forceRender
函数强制重新渲染,第二次调用resolveAsyncComponent
函数,返回真正的组件构造函数factory.resolved
,再通过组件更新的 patch 过程就能渲染出异步组件了。