阅读 2287

🚩Vue源码——异步组件是如何使用

最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。

前言

在上一篇🚩Vue源码——组件是如何注册和使用中,详细介绍了组件注册和使用的内部逻辑流程,里面所介绍都是同步组件的注册和使用。但是在实际开发中,异步组件才是经常使用的。先来看一下,官方文档中是如何注册异步组件,可以分为三种方式。

  • 普通函数异步组件
Vue.component('aa', function(resolve, reject) {
    setTimeout(function() {
        resolve({
            template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
            data() {
                return {
                    aa: '欢迎',
                    bb: 'Vue'
                }
            }
        })
    }, 1000)
})
复制代码
  • Promise 异步组件
Vue.component('aa', () => import('./aa.js') )
复制代码
  • 高级异步组件
const aa = () => ({
    // 需要加载的组件。应当是一个 Promise
    component: import('./aa.vue'),
    // 加载中应当渲染的组件
    loading: LoadingComp,
    // 出错时渲染的组件
    error: ErrorComp,
    // 渲染加载中组件前的等待时间。默认:200ms。
    delay: 200,
    // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
    timeout: 3000
})
Vue.component('aa', aa)
复制代码

从以上示例中可以看到,通过Vue.component注册的组件不再是一个对象,而是一个函数,这个函数也不是组件构造函数,是一个工厂函数。这个工厂函数有两个参数resolve函数和reject函数,其是 Vue 内部定义的,在这个工厂函数中有个异步函数,当异步函数执行成功后调用resolve函数,其参数就是异步组件的对象。

从上一篇🚩Vue源码——组件是如何注册和使用中,可以得知组件的使用,要先在vm._render过程中执行vnode = createComponent(Ctor, data, context, children, tag)生成vnode,其中参数Ctor可以是函数或对象,从createComponent方法开始介绍异步组件是如何使用。

function createComponent(Ctor, data, context, children, tag) {
    if (isUndef(Ctor)) {
        return
    }
    var baseCtor = context.$options._base;
    if (isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor);
    }
    if (typeof Ctor !== 'function') {
        return
    }
    // async component
    var asyncFactory;
    if (isUndef(Ctor.cid)) {
        asyncFactory = Ctor;
        Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
        if (Ctor === undefined) {
            return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
        }
    }
    data = data || {};
    resolveConstructorOptions(Ctor);
    installComponentHooks(data);
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
        ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
        data, undefined, undefined, undefined, context, {
            Ctor: Ctor,
            tag: tag,
            children: children
        },
        asyncFactory
    );
    return vnode
}
复制代码

当参数Ctor值的类型是函数时,不会执行Ctor = baseCtor.extend(Ctor)

因为在 Vue 中是调用Vue.extend方法来创建继承 Vue 的组件构造函数。在Vue.extend中会执行Sub.cid = cid++给组件构造函数的cid属性赋值。

var cid = 1;
Vue.extend = function(extendOptions) {
    var Sub = function VueComponent(options) {
        this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    return Sub
}
复制代码

所以可以用isUndef(Ctor.cid)来判断Ctor是不是一个组件构造函数,若不是执行Ctor = resolveAsyncComponent(asyncFactory, baseCtor)进入异步组件使用的逻辑,先来看一下resolveAsyncComponent函数。

function resolveAsyncComponent(factory, baseCtor) {
    if (isTrue(factory.error) && isDef(factory.errorComp)) {
        return factory.errorComp
    }
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
    var 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)) {
        var owners = factory.owners = [owner];
        var sync = true;
        var timerLoading = null;
        var timerTimeout = null;
        (owner).$on('hook:destroyed', function() {
            return remove(owners, owner);
        });
        var forceRender = function(renderCompleted) {
            for (var 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;
                }
            }
        };
        var resolve = once(function(res) {
            factory.resolved = ensureCtor(res, baseCtor);
            if (!sync) {
                forceRender(true);
            } else {
                owners.length = 0;
            }
        });
        var reject = once(function(reason) {
            warn(
                "Failed to resolve async component: " + (String(factory)) +
                (reason ? ("\nReason: " + reason) : '')
            );
            if (isDef(factory.errorComp)) {
                factory.error = true;
                forceRender(true);
            }
        });
        var 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(function() {
                            timerLoading = null;
                            if (isUndef(factory.resolved) && isUndef(factory.error)) {
                                factory.loading = true;
                                forceRender(false);
                            }
                        }, res.delay || 200);
                    }
                }
                if (isDef(res.timeout)) {
                    timerTimeout = setTimeout(function() {
                        timerTimeout = null;
                        if (isUndef(factory.resolved)) {
                            reject(
                                "timeout (" + (res.timeout) + "ms)"
                            );
                        }
                    }, res.timeout);
                }
            }
        }
        sync = false;
        return factory.loading ? factory.loadingComp : factory.resolved
    }
}
复制代码

resolveAsyncComponent函数,是个高阶函数,主要对注册异步组件时,传入不同的工厂函数进行处理,内部定义了工厂函数的参数resolve函数和reject函数,并调用了工厂函数,成功执行resolve函数,失败执行reject函数,最后返回组件构造函数或 undefined。

异步组件和同步组件的注册原理是一样,只是异步组件的使用原理跟同步组件是不一样的。下面来按三种注册异步组件的方式来一一分析介绍异步组件的使用原理。

一、普通函数异步组件

Vue.component('aa', function(resolve, reject) {
    setTimeout(function() {
        resolve({
            template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
            data() {
                return {
                    aa: '欢迎',
                    bb: 'Vue'
                }
            }
        })
    }, 1000)
})
复制代码

resolveAsyncComponent(factory, baseCtor),参数factory的值就是上面Vue.component的第二参数,参数baseCtor是 Vue 构造函数。整理一下代码,跟此场景无关的代码都去掉。

function resolveAsyncComponent(factory, baseCtor) {
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
    var owner = currentRenderingInstance;
    if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
        factory.owners.push(owner);
    }
    if (owner && !isDef(factory.owners)) {
        var owners = factory.owners = [owner];
        var sync = true;
        (owner).$on('hook:destroyed', function() {
            return remove(owners, owner);
        });
        var forceRender = function(renderCompleted) {
            for (var i = 0, l = owners.length; i < l; i++) {
                (owners[i]).$forceUpdate();
            }
            if (renderCompleted) {
                owners.length = 0;
            }
        };
        var resolve = once(function(res) {
            factory.resolved = ensureCtor(res, baseCtor);
            if (!sync) {
                forceRender(true);
            } else {
                owners.length = 0;
            }
        });
        var reject = once(function(reason) {
            warn(
                "Failed to resolve async component: " + (String(factory)) +
                (reason ? ("\nReason: " + reason) : '')
            );
        });
        var res = factory(resolve, reject);
        sync = false;
        return factory.loading ? factory.loadingComp : factory.resolved
    }
}
复制代码

resolveAsyncComponent函数中,内部定义了三个函数forceRenderresolvereject。其中resolvereject函数是用once函数包装。

function once(fn) {
    var called = false;
    return function() {
        if (!called) {
            called = true;
            fn.apply(this, arguments);
        }
    }
}
复制代码

once函数是个高阶函数,巧妙利用闭包和called变量,保证所包装的函数只执行一次。也就是确保resolvereject函数只执行一次。

因为在resolveAsyncComponent函数中最后执行return factory.loading ? factory.loadingComp : factory.resolved,返回factory.resolved

所以来看一下factory.resolved,其定义在resolve函数中。

var resolve = once(function(res) {
    factory.resolved = ensureCtor(res, baseCtor);
    if (!sync) {
        forceRender(true);
    } else {
        owners.length = 0;
    }
})
复制代码

执行ensureCtor(res, baseCtor)后赋值给factory.resolved,来看一下ensureCtor方法。

function ensureCtor(comp, base) {
    if (
        comp.__esModule ||
        (hasSymbol && comp[Symbol.toStringTag] === 'Module')
    ) {
        comp = comp.default;
    }
    return isObject(comp) ?
        base.extend(comp) :
        comp
}
复制代码

参数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的第二参数传参的,值如下所示。

function(resolve, reject) {
    setTimeout(function() {
        resolve({
            template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
            data() {
                return {
                    aa: '欢迎',
                    bb: 'Vue'
                }
            }
        })
    }, 1000)
}
复制代码

其中resolve就是resolveAsyncComponent函数内部定义的resolve函数。那么参数comp的值如下所示,是个组件的选项对象。

{
    template: '<div><p>{{aa}}<span>{{bb}}</span></p></div>',
    data() {
        return {
            aa: '欢迎',
            bb: 'Vue'
        }
    }
}
复制代码

这样执行ensureCtor(res, baseCtor)会得到一个组件构造函数,那么factory.resolved的值为一个组件构造函数。

再回到createComponent方法中,看这段代码

Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
if (Ctor === undefined) {
    return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
}
复制代码

执行resolveAsyncComponent方法后返回一个组件构造函数赋值给Ctor。就这么结束了吗。当然不是了,不知你有没有注意到在Vue.component定义的第二参数中,resolve(//...)外层还有一个setTimeout定时器,是个异步任务。JavaScript 是单线程的,异步任务要等所有同步任务都执行完才能执行。故此时resolveAsyncComponent方法中的resolve函数是不执行,factory.resolved应该为 undefined 。那么Ctor为 undefined ,要执行return createAsyncPlaceholder(asyncFactory, data, context, children, tag)createAsyncPlaceholder方法是用来创建一个注释节点vnode作为占位符。

function createAsyncPlaceholder(factory, data, context, children, tag) {
    var node = createEmptyVNode();
    node.asyncFactory = factory;
    node.asyncMeta = {
        data: data,
        context: context,
        children: children,
        tag: tag
    };
    return node
}
复制代码

此时createComponent方法生成的一个注释节点vnode,而不是一个组件vnode,那组件要怎么渲染,不着急,再回到resolveAsyncComponent方法中,在 return 之前执行sync = false,在1000ms后resolve函数执行,会执行forceRender(true),来看一下forceRender函数。

var resolve = once(function(res) {
    factory.resolved = ensureCtor(res, baseCtor);
    if (!sync) {
        forceRender(true);
    } else {
        owners.length = 0;
    }
})
var owner = currentRenderingInstance;
if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
    factory.owners.push(owner);
}
if (owner && !isDef(factory.owners)) {
    var owners = factory.owners = [owner];
    var forceRender = function(renderCompleted) {
        for (var i = 0, l = owners.length; i < l; i++) {
            (owners[i]).$forceUpdate();
        }
        if (renderCompleted) {
            owners.length = 0;
        }
    };
}
复制代码

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 实例重新渲染一次。

Vue.prototype.$forceUpdate = function() {
    var vm = this;
    if (vm._watcher) {
        vm._watcher.update();
    }
}
复制代码

执行vm._watcher.update()相当执行mountComponent方法中的vm._update(vm._render(), hydrating),在执行vm._render()过程中调用createComponent方法又执行到以下逻辑。

// async component
var asyncFactory;
if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
        return createAsyncPlaceholder(asyncFactory, data, context, children, tag)
    }
}
复制代码

再次执行resolveAsyncComponent(asyncFactory, baseCtor)时,1000ms已过,故异步组件注册的工厂函数factory中的resolve函数已经执行完毕,故factory.resolved有值,直接返回factory.resolved

function resolveAsyncComponent(factory, baseCtor) {
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
}
复制代码

返回值factory.resolved是一个异步组件构造函数赋值给CtorCtor的值获取到后,生成组件vnode,再执行vm._update,进入 patch 过程,把原先生成的组件占位注释节点替换成真正的组件 DOM 内容 ,这里不介绍组件更新的 patch 过程,后续会开一个单章来介绍。到这里普通函数异步组件的使用原理介绍完毕,下面来介绍Promise 异步组件的使用原理。

二、Promise 异步组件

Vue.component('aa', () => import('./aa.js') )
复制代码

resolveAsyncComponent(factory, baseCtor),参数baseCtor是 Vue 构造函数。参数factory的值就是上面Vue.component的第二参数,返回值是import('./aa.js'),它是一个 Promise 对象。整理一下代码,跟此场景无关的代码都去掉。

function resolveAsyncComponent(factory, baseCtor) {
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
    var owner = currentRenderingInstance;
    if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) {
        factory.owners.push(owner);
    }
    if (owner && !isDef(factory.owners)) {
        var owners = factory.owners = [owner];
        var sync = true;
        (owner).$on('hook:destroyed', function() {
            return remove(owners, owner);
        });
        var forceRender = function(renderCompleted) {
            for (var i = 0, l = owners.length; i < l; i++) {
                (owners[i]).$forceUpdate();
            }
            if (renderCompleted) {
                owners.length = 0;
            }
        };
        var resolve = once(function(res) {
            factory.resolved = ensureCtor(res, baseCtor);
            if (!sync) {
                forceRender(true);
            } else {
                owners.length = 0;
            }
        });
        var reject = once(function(reason) {
            warn(
                "Failed to resolve async component: " + (String(factory)) +
                (reason ? ("\nReason: " + reason) : '')
            );
            if (isDef(factory.errorComp)) {
                factory.error = true;
                forceRender(true);
            }
        });
        var res = factory(resolve, reject);
        if (isObject(res)) {
            if (isPromise(res)) {
                if (isUndef(factory.resolved)) {
                    res.then(resolve, reject);
                }
            }
        }
        sync = false;
        return factory.loading ? factory.loadingComp : factory.resolved
    }
}
复制代码

因为在此场景中工厂函数factory的返回值是一个 Promise 对象,所以满足isObject(res)isPromise(res)的条件,执行以下逻辑

if (isUndef(factory.resolved)) {
    res.then(resolve, reject);
}
复制代码

因为返回的是 Promise 对象,其实例方法then的参数是两个的函数resolvereject。当执行成功会执行resolve函数,当执行失败会调用reject函数。

所以这里巧妙地执行res.then(resolve, reject),当执行成功后会掉resolve函数,而这个resolve函数,是resolveAsyncComponent函数中自定义的。接下来的逻辑就和普通函数异步组件一模一样。

三、高级异步组件

在高级异步组件中,可定义异步组件加载中展示的组件和加载失败展示的组件,对用户更友好。

const aa = () => ({
    // 需要加载的组件。应当是一个 Promise
    component: import('./aa.vue'),
    // 加载中展示的组件
    loading: LoadingComp,
    // 加载失败展示的组件
    error: ErrorComp,
    // 渲染加载中组件前的等待时间。默认:200ms。
    delay: 200,
    // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
    timeout: 3000
})
Vue.component('aa', aa)
复制代码

resolveAsyncComponent(factory, baseCtor),参数baseCtor是 Vue 构造函数。参数factory的值就是上面Vue.component的第二参数,返回值是一个对象。整理一下代码,跟此场景无关的代码都去掉。

function resolveAsyncComponent(factory, baseCtor) {
    if (isTrue(factory.error) && isDef(factory.errorComp)) {
        return factory.errorComp
    }
    if (isDef(factory.resolved)) {
        return factory.resolved
    }
    var 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)) {
        var owners = factory.owners = [owner];
        var sync = true;
        var timerLoading = null;
        var timerTimeout = null;
        (owner).$on('hook:destroyed', function() {
            return remove(owners, owner);
        });

        var forceRender = function(renderCompleted) {
            for (var 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;
                }
            }
        };

        var resolve = once(function(res) {
            factory.resolved = ensureCtor(res, baseCtor);
            if (!sync) {
                forceRender(true);
            } else {
                owners.length = 0;
            }
        });

        var reject = once(function(reason) {
            warn(
                "Failed to resolve async component: " + (String(factory)) +
                (reason ? ("\nReason: " + reason) : '')
            );
            if (isDef(factory.errorComp)) {
                factory.error = true;
                forceRender(true);
            }
        });

        var res = factory(resolve, reject);

        if (isObject(res)) {
            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(function() {
                            timerLoading = null;
                            if (isUndef(factory.resolved) && isUndef(factory.error)) {
                                factory.loading = true;
                                forceRender(false);
                            }
                        }, res.delay || 200);
                    }
                }

                if (isDef(res.timeout)) {
                    timerTimeout = setTimeout(function() {
                        timerTimeout = null;
                        if (isUndef(factory.resolved)) {
                            reject(
                                "timeout (" + (res.timeout) + "ms)"
                            );
                        }
                    }, res.timeout);
                }
            }
        }

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

复制代码

因为在此场景中工厂函数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方法。

if (isTrue(factory.error) && isDef(factory.errorComp)) {
    return factory.errorComp
}
if (isTrue(factory.loading) && isDef(factory.loadingComp)) {
    return factory.loadingComp
}
复制代码

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 过程就能渲染出异步组件了。