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

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

异步组件常用有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. node 连接 mysql 数据库三种方法------笔记

    一.mysql库 文档:https://github.com/mysqljs/mysql mysql有三种创建连接方式 1.createConnection 使用时需要对连接的创建.断开进行管理 2. ...

  2. webwork遍历数组标签

    WebWork中提供了一个<ww:iterator></ww:iterator>标签用于遍历数组. 01 如果数组中是普通类型,比如String.int等类型,可以通过标签中的 ...

  3. Redis for OPS 03:数据安全与持久化

    写在前面的话 通过前两节,除了安装部分,其它的更多的是作为了解,除非我们面向实际的开发,当然知道更多总是好的,这样才有吹牛逼的资本. 从本节开始我们主要谈谈作为一个运维,在处理 Redis 的维护的时 ...

  4. 点云3D 目标检测

    点云 点云是雷达采集到的信息. 关于点云基本介绍参考https://zhuanlan.zhihu.com/p/22581673 ros中的点云消息结构:http://docs.ros.org/jade ...

  5. node.js箭头函数使用

    ES6允许使用=>定义函数,箭头函数使得表达更加简洁,例如 // 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1 ...

  6. MVC过滤器:自定义异常过滤器

    一.异常过滤器 异常筛选器用于实现IExceptionFilter接口,并在ASP.NET MVC管道执行期间引发了未处理的异常时执行.异常筛选器可用于执行诸如日志记录或显示错误页之类的任务.Hand ...

  7. Wpf,Unity6

    <?xml version="1.0" encoding="utf-8"?><packages> <package id=&quo ...

  8. Vue介绍以及模板语法-插值

    1.Vue的介绍 Vue是一套用于构建用户界面的渐进式框架. 注意:Vue是一个框架,相对于jq库来说,是由本质的区别的:https://cn.vuejs.org/ Vue不支持IE8及一下版本,因为 ...

  9. TCP协议如何保证可靠传输?

    一.TCP的可靠传输如何保证? 在TCP连接中,数据流必须以正确的顺序传送给对方.TCP的可靠性是通过顺序编号和确认(ACK)实现的.TCP在开始传送一个段时,为准备重传而首先将该段插入到发送队列中, ...

  10. 使用CAD快速看图如何将图纸打印和预览?

    有相关CAD工作经验的小伙伴们都知道,绘制完CAD图纸后是需要借助CAD看图工具来进行查看图纸的,其实CAD快速看图中不仅能够对图纸进行查看,还能够将CAD图纸进行打印出来.但是有很多的伙伴不知道要怎 ...