当我们的项目足够大,使用的组件就会很多,此时如果一次性加载所有的组件是比较花费时间的。一开始就把所有的组件都加载是没必要的一笔开销,此时可以用异步组件来优化一下。

异步组件简单的说就是只有等到在页面里显示该组件的时候才会从服务器加载,不显式的话就不会加载,这样即可提高客户端的访问速度也可以降低对服务器的请求次数,可谓优化的一个利器。

异步组件常用有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 ? ("\nReason: " + 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了,后面的步骤和工厂函数的流程是一样的。

三:高级异步组件

高级异步组件可以定义更多的状态,比如加载该组件的超时时间、加载过程中显式的组件、出错时显式的组件、延迟时间等

writer by:大沙漠 QQ:22969969

高级异步组件也是定义一个函数,返回值是一个对象,对象的每个属性在官网说得挺详细的了,如下,连接::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,搞定,流程就这样吧

Vue.js 源码分析(二十七) 高级应用 异步组件 详解的更多相关文章

  1. Vue.js 源码分析(三十) 高级应用 函数式组件 详解

    函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的<router-view>组件就是一个函 ...

  2. Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

    当使用is特性切换不同的组件时,每次都会重新生成组件Vue实例并生成对应的VNode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如: <!DOCTYPE html> ...

  3. Vue.js 源码分析(二十三) 指令篇 v-show指令详解

    v-show的作用是将表达式值转换为布尔值,根据该布尔值的真假来显示/隐藏切换元素,它是通过切换元素的display这个css属性值来实现的,例如: <!DOCTYPE html> < ...

  4. Vue.js 源码分析(二十一) 指令篇 v-pre指令详解

    该指令会跳过所在元素和它的子元素的编译过程,也就是把这个节点及其子节点当作一个静态节点来处理,例如: <!DOCTYPE html> <html lang="en" ...

  5. Vue.js 源码分析(二十) 指令篇 v-once指令详解

    数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值,例如:<p>Message: {{ msg }}</p>以后每当msg属性发生了改变,插值处的内 ...

  6. Vue.js 源码分析(十八) 指令篇 v-for 指令详解

    我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!DOCTYPE html> <html lang="en"> & ...

  7. Vue.js 源码分析(十六) 指令篇 v-on指令详解

    可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: <!DOCTYPE html> <html lang="en"& ...

  8. Vue.js 源码分析(十五) 指令篇 v-bind指令详解

    指令是Vue.js模板中最常用的一项功能,它带有前缀v-,比如上面说的v-if.v-html.v-pre等.指令的主要职责就是当其表达式的值改变时,相应的将某些行为应用到DOM上,先介绍v-bind指 ...

  9. Vue.js 源码分析(十一) 基础篇 过滤器 filters属性详解

    Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化.过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持).过滤器应该被添加在 JavaScrip ...

随机推荐

  1. Android高可用移动网络连接---(转载自http://wingjay.com/2019/01/16/mobile-network-connection/)

    读者好,前面我们在 <Android 架构之网络连接与加速> 和<Android 架构之长连接技术>两篇文章中,讲解了 Http 短连接.TCP 长连接.连接复用与速度优化.数 ...

  2. make 命令与 Makefile

    make 是一个工具程序,通过读取 Makefile 文件,实现自动化软件构建.虽然现代软件开发中,集成开发环境已经取代了 make,但在 Unix 环境中,make 仍然被广泛用来协助软件开发.ma ...

  3. Abp vNext框架 实例程序BookStore-笔记

    参考 Abp vNext框架 应用程序开发教程 创建项目和书籍列表页面 http://www.vnfan.com/helinbin/d/3579c6e90e1d23ab.html 官方源码 https ...

  4. MySQL问题记录——导入导出权限设置

    MySQL问题记录——导入导出权限设置 摘要:本文主要记录了在使用MySQL的过程中导入导出权限设置时遇到的问题以及解决方案. 相关日志 [Note] --secure-file-priv is se ...

  5. 何为http?何为RPC?

    RPC(即Remote Procedure Call,远程过程调用)和HTTP(HyperText Transfer Protocol,超文本传输协议)他们最本质的区别,就是RPC主要工作在TCP协议 ...

  6. verdaccio启动命令

    1.启动项目 根目录下 verdaccio 2.npm 源管理 nrm 下载 3.nrm 源列表 nrm ls 4.nrm 源增加 nrm add verdaccio http://localhost ...

  7. uni-app自定义Modal弹窗组件|仿ios、微信弹窗效果

    介绍 uniapp自定义弹窗组件uniPop,基于uni-app开发的自定义模态弹窗|msg信息框|alert对话框|confirm确认框|toast弱提示框 支持多种动画效果.多弹窗类型ios/an ...

  8. 使用Wireshark进行DNS协议解析

    - 域名及解析过程 域名由一系列 - DNS协议报文格式 一次DNS过程包含一对请求报文和响应报文.请求和响应报文有统一的报文格式如下图: - DNS报文例子 一次DNS请求的过程: 包括请求和响应, ...

  9. 【JavaWeb】jQuery对Ajax的支持

    jQuery对Ajax的支持 jQuery对Ajax进行封装,提供了$.ajax()方法 语法:$.ajax(options) 常用设置项 说明 url 发送请求地址 type 请求类型get|pos ...

  10. Mysql添加用户与授权

    1.本地环境 CentOS Linux release 7.5.1804 (Core) mysql Ver 14.14 Distrib 5.7.22, for Linux (x86_64) using ...