异步组件三种方式
- 普通函数异步组件
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 过程就能渲染出异步组件了。


