当我们的项目足够大,使用的组件就会很多,此时如果一次性加载所有的组件是比较花费时间的。一开始就把所有的组件都加载是没必要的一笔开销,此时可以用异步组件来优化一下。
异步组件简单的说就是只有等到在页面里显示该组件的时候才会从服务器加载,不显式的话就不会加载,这样即可提高客户端的访问速度也可以降低对服务器的请求次数,可谓优化的一个利器。
异步组件常用有3种异步组件的实现:工厂函数、Promise加载和高级异步组件。
注:一般的项目都是在vue-router的路由里面创建vue-router实例时通过routes属性指定路由的,其实在vue里面也可以实现。
OK,开干,先搭建一个环境,我们先用Vue-li3搭建一个脚手架 ,默认的配置搭建完后在浏览器输入:http://localhost:8080即可打开页面,默认部分如下:
页面下部分显式的就不截图了,然后点击about可以切换路由,为了测试我们对异步组件的分析,我们把main.js和app.js和/src/components/HelloWorld.vue进行改写,如下:
对于/src/components/HelloWorld.vue组件,为了我们测试更方便,直接更改为:
<template> <div class="hello"> <p>Hello World!</p> </div> </template>
只显示Hello World!就好了,对于main.js文件,修改如下:
修改前的内容为:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app')
修改为:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
import helloworld from './components/HelloWorld.vue'
Vue.component('HelloWorld',helloworld)
new Vue({
router,
render: h => h(App)
}).$mount('#app')
修改后HelloWorld作为一个全局的组件形式存在。然后修改app.vue文件
修改前的内容为:
<template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view/> </div> </template>
我们把它修改为:
<template> <div id="app"> <button @click="show=true">Test</button> <HelloWorld v-if="show"></HelloWorld> </div> </template> <script> export default{ data(){ return{ show:false } } } </script>
渲染后的页面为:
当我们点击Test这个按钮时,Hello World组件就会显式出来,如下:
这里我们定义的Vue.component('HelloWorld',helloworld)是一个常规组件,非异步组件,下面我们通过修改main.js来模拟不同的异步组件例子,然后通过代码去看看它的实现原理
一:工厂函数
Vue.js允许将组件定义为一个工厂函数,动态的解析组件,Vue.js只在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。
例如我们把main.js修改成这样:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false Vue.component('HelloWorld',function(resolve,reject){ //重写HelloWorld组件的定义 require(['./components/HelloWorld'],function(res){ resolve(res) }) }) new Vue({ router, render: h => h(App) }).$mount('#app')
只有当我们点击Test这个按钮时这个组件才会加载进来
源码分析
当组件执行_render函数转换成虚拟VNode时遇到组件时会执行createComponent()函数,如下:
function createComponent ( //第4184行 创建组件Vnode Ctor, //Ctor:组件的构造函数 data, //data:数组 context, //context:Vue实例 children, //child:组件的子节点 tag ) { if (isUndef(Ctor)) { return } var baseCtor = context.$options._base; // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(("Invalid Component definition: " + (String(Ctor))), context); } return } // async component var asyncFactory; if (isUndef(Ctor.cid)) { //如果Ctor.cid为空,那么Ctor就是一个函数,表明这是一个异步组件 asyncFactory = Ctor; //获取异步组件的函数 Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context); //执行resolveAsyncComponent()函数 if (Ctor === undefined) { //如果Ctor是个空的,调用该函数返回一个空的注释节点 // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } /*略*/ return vnode }
对于一个组件来说,比如Vue.component(component-name,obj|func),组件的值可以是一个对象,也可以是一个函数,如果是对象,则注册时会执行Vue.extend()函数,如下:
if (type === 'component' && isPlainObject(definition)) { //第4866行 注册组件时,如果组件是个对象,则执行Vue.extend() definition.name = definition.name || id; definition = this.options._base.extend(definition); }
去构造子组件的基础构造函数,此时会在构造函数上新增一个cid属性(在4789行),所以我们这里通过cid来判断该组件是否为一个函数。
回到主线,接着执行resolveAsyncComponent()函数,工厂函数相关的如下:
function resolveAsyncComponent ( //第2283行 异步组件 factory:异步组件的函数 baseCtor:大Vue context:当前的Vue实例 factory, baseCtor, context ) { if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { //工厂函数异步组件第二次执行这里时会返回factory.resolved return factory.resolved } if (isTrue(factory.loading) && isDef(factory.loadingComp)) { return factory.loadingComp } if (isDef(factory.contexts)) { // already pending factory.contexts.push(context); } else { var contexts = factory.contexts = [context]; //将context作为数组保存到contexts里,也就是当前Vue实例 var sync = true; var forceRender = function () { //遍历contexts里的所有元素 下一个tick执行到这里 for (var i = 0, l = contexts.length; i < l; i++) { //依次调用该元素的$forceUpdate()方法 该方法会强制渲染一次 contexts[i].$forceUpdate(); } }; var resolve = once(function (res) { //定义一个resolve函数 // cache resolved factory.resolved = ensureCtor(res, baseCtor); // invoke callbacks only if this is not a synchronous resolve // (async resolves are shimmed as synchronous during SSR) if (!sync) { forceRender(); } }); var reject = once(function (reason) { //定义一个reject函数 "development" !== 'production' && warn( "Failed to resolve async component: " + (String(factory)) + (reason ? (" Reason: " + reason) : '') ); if (isDef(factory.errorComp)) { factory.error = true; forceRender(); } }); var res = factory(resolve, reject); //执行factory()函数 if (isObject(res)) { /*高级组件的逻辑*/ } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
resolveAsyncComponent内部会定义一个resolve和reject函数,然后执行factory()函数,factory()就是我们在main.js里给HelloWorld组件定义的函数,函数内会执行require函数,由于require()是个异步操作,所以resolveAsyncComponent就会返回undefined
回到resolveAsyncComponent,我们给factory()函数的执行下一个断点,如下:
可以看到返回一个undefined,最后resolveAsyncComponent()也会返回undefined,回到createComponent()函数,由于返回的是undefined,则会执行createAsyncPlaceholder()去创建一个注释节点,渲染后对应的DOM节点树如下:
可以看到对于工厂函数来说,组件完全加载时对应的DOM节点是一个注释节点
在下一个tick等require()加载成功后就会执行resolve(res)函数,也就是在resolveAsyncComponent()内定义的resolve函数,
resolve函数会将结果保存到工厂函数的resolved属性里(也就是组件的定义)然后执行的forceRender()函数,也就是上面标记的蓝色的注释对应的代码
再次重新渲染执行到resolveAsyncComponent的时候此时局部变量factory.resolved存在了,就直接返回该变量, 如下:
此时就会走组件的常规逻辑,进行渲染组件了。
二:Promise加载
Promise()比较简单,可以认为是工厂函数扩展成语法糖的知识,他主要是可以很好的配合webpack的语法糖,webpack的import的语法糖就是返回一个promise对象,Vue实际上做异步组件也是为了配合Webpack的语法糖来实现Promise()的趋势。
例如我们把main.js改成如下的:
import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false Vue.component('HelloWorld',()=>import('./components/HelloWorld')) new Vue({ router, render: h => h(App) }).$mount('#app')
和工厂函数一样,也会执行两次resolveAsyncComponent,下一个tick的逻辑是一样的,不一样的是触发resolve()的逻辑不通,如下:
源码分析
function resolveAsyncComponent ( //异步组件 factory, baseCtor, context ) { if (isTrue(factory.error) && isDef(factory.errorComp)) { return factory.errorComp } if (isDef(factory.resolved)) { //第一次执行到这里时factory.resolved也不存在 return factory.resolved } /*略*/ var res = factory(resolve, reject); //我们这里返回一个含有then的对象 if (isObject(res)) { if (typeof res.then === 'function') { //如果res是一个函数,即Promise()方式加载时 // () => Promise if (isUndef(factory.resolved)) { //如果factory.resolved不存在 res.then(resolve, reject); //用then方法指定resolve和reject的回调函数 } } else if (isDef(res.component) && typeof res.component.then === 'function') { /**/ } } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
例子里执行到factory()后返回的res对象如下:
等到加载成功后就会执行resolve了,后面的步骤和工厂函数的流程是一样的。
三:高级异步组件
高级异步组件可以定义更多的状态,比如加载该组件的超时时间、加载过程中显式的组件、出错时显式的组件、延迟时间等
高级异步组件也是定义一个函数,返回值是一个对象,对象的每个属性在官网说得挺详细的了,如下,连接::https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81
对于高级异步组件来说,他和promise()方法加载的逻辑是一样的,不同的是多了几个属性,如下:
源码分析
function resolveAsyncComponent ( //第2283行 异步组件 factory, baseCtor, context ) { /*略*/ if (isObject(res)) { if (typeof res.then === 'function') { //promise的分支 // () => Promise if (isUndef(factory.resolved)) { res.then(resolve, reject); } } else if (isDef(res.component) && typeof res.component.then === 'function') { //高级异步组件的分支 res.component.then(resolve, reject); //还是调用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) { //如果等待时间为0 factory.loading = true; //直接设置factory.loading为true } else { setTimeout(function () { if (isUndef(factory.resolved) && isUndef(factory.error)) { factory.loading = true; forceRender(); } }, res.delay || 200); } } if (isDef(res.timeout)) { //超时时间 setTimeout(function () { if (isUndef(factory.resolved)) { reject( process.env.NODE_ENV !== 'production' ? ("timeout (" + (res.timeout) + "ms)") : null ); } }, res.timeout); } } } sync = false; // return in case resolved synchronously return factory.loading ? factory.loadingComp : factory.resolved } }
OK,搞定,流程就这样吧