vue3的defineAsyncComponent是如何实现异步组件的呢?
前言
在上一篇 给我5分钟,保证教会你在vue3中动态加载远程组件文章中,我们通过defineAsyncComponent
实现了动态加载远程组件。这篇文章我们将通过debug源码的方式来带你搞清楚defineAsyncComponent
是如何实现异步组件的。注:本文使用的vue版本为3.4.19
欧阳写了一本开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。完全免费,只求一个star。
看个demo
还是一样的套路,我们来看个defineAsyncComponent
异步组件的demo。
本地子组件local-child.vue
代码如下:
<template>
<p>我是本地组件</p>
</template>
异步子组件async-child.vue
代码如下:
<template>
<p>我是异步组件</p>
</template>
父组件index.vue
代码如下:
<template>
<LocalChild />
<button @click="showAsyncChild = true">load async child</button>
<AsyncChild v-if="showAsyncChild" />
</template>
<script setup lang="ts">
import { defineAsyncComponent, ref } from "vue";
import LocalChild from "./local-child.vue";
const AsyncChild = defineAsyncComponent(() => import("./async-child.vue"));
const showAsyncChild = ref(false);
</script>
我们这里有两个子组件,第一个local-child.vue
,他和我们平时使用的组件一样,没什么说的。
第二个子组件是async-child.vue
,在父组件中我们没有像普通组件local-child.vue
那样在最上面import导入,而是在defineAsyncComponent
接收的回调函数中去动态import导入async-child.vue
文件,这样定义的AsyncChild
组件就是异步组件。
在template中可以看到,只有当点击load async child
按钮后才会加载异步组件AsyncChild
。
我们先来看看执行效果,如下gif图:
从上面的gif图可以看到,当我们点击load async child
按钮后,在network面板中才会去加载异步组件async-child.vue
。
defineAsyncComponent
除了像上面这样直接接收一个返回Promise的回调函数之外,还可以接收一个对象作为参数。demo代码如下:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./async-child.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
其中对象参数有几个字段:
loader
字段其实对应的就是前面那种写法中的回调函数。loadingComponent
为加载异步组件期间要显示的loading组件。delay
为显示loading组件的延迟时间,默认200ms。这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。errorComponent
为加载失败后显示的组件。timeout
为超时时间。
在接下来的源码分析中,我们还是以前面那个接收一个返回Promise的回调函数为例子进行debug调试源码。
开始打断点
我们在浏览器中接着来看父组件index.vue
编译后的代码,很简单,在浏览器中可以像vscode一样使用command(windows中是control)+p就可以唤起一个输入框,然后在输入框中输入index.vue
点击回车就可以在source面板中打开编译后的index.vue
文件了。如下图:
我们看到编译后的index.vue
文件代码如下:
import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=868545d8";
import {
defineAsyncComponent,
ref,
} from "/node_modules/.vite/deps/vue.js?v=868545d8";
import LocalChild from "/src/components/defineAsyncComponentDemo/local-child.vue?t=1723193310324";
const _sfc_main = _defineComponent({
__name: "index",
setup(__props, { expose: __expose }) {
__expose();
const showAsyncChild = ref(false);
const AsyncChild = defineAsyncComponent(() =>
import("/src/components/defineAsyncComponentDemo/async-child.vue")
);
const __returned__ = { showAsyncChild, AsyncChild, LocalChild };
return __returned__;
},
});
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
// ...省略
}
export default _export_sfc(_sfc_main, [["render", _sfc_render]]);
从上面的代码可以看到编译后的index.vue
主要分为两块,第一块为_sfc_main
对象中的setup
方法,对应的是我们的script
模块。第二块为_sfc_render
,也就是我们常说的render函数,对应的是template中的内容。
我们想要搞清楚defineAsyncComponent
方法的原理,那么当然是给setup方法中的defineAsyncComponent
方法打断点。刷新页面,此时代码将会停留在断点defineAsyncComponent
方法处。
defineAsyncComponent
方法
然后将断点走进defineAsyncComponent
函数内部,在我们这个场景中简化后的defineAsyncComponent
函数代码如下:
function defineAsyncComponent(source) {
if (isFunction(source)) {
source = { loader: source };
}
const { loader, loadingComponent, errorComponent, delay = 200 } = source;
let resolvedComp;
const load = () => {
return loader()
.catch(() => {
// ...省略
})
.then((comp) => {
if (
comp &&
(comp.__esModule || comp[Symbol.toStringTag] === "Module")
) {
comp = comp.default;
}
resolvedComp = comp;
return comp;
});
};
return defineComponent({
name: "AsyncComponentWrapper",
setup() {
const instance = currentInstance;
const loaded = ref(false);
const error = ref();
const delayed = ref(!!delay);
if (delay) {
setTimeout(() => {
delayed.value = false;
}, delay);
}
load()
.then(() => {
loaded.value = true;
})
.catch((err) => {
onError(err);
error.value = err;
});
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
},
});
}
从上面的代码可以看到defineAsyncComponent
分为三部分。
第一部分为:处理传入的参数。
第二部分为:
load
函数用于加载异步组件。第三部分为:返回
defineComponent
定义的组件。
第一部分:处理传入的参数
我们看第一部分:处理传入的参数。代码如下:
function defineAsyncComponent(source) {
if (isFunction(source)) {
source = { loader: source };
}
const { loader, loadingComponent, errorComponent, delay = 200 } = source;
let resolvedComp;
// ...省略
}
首先使用isFunction(source)
判断传入的source
是不是函数,如果是函数,那么就将source
重写为包含loader
字段的对象:source = { loader: source }
。然后使用const { loader, loadingComponent, errorComponent, delay = 200 } = source
解构出对应的loading组件、加载失败组件、延时时间。
看到这里我想你应该明白了为什么defineAsyncComponent
函数接收的参数可以是一个回调函数,也可以是包含loader
、loadingComponent
、errorComponent
等字段的对象。因为如果我们传入的是回调函数,在内部会将传入的回调函数赋值给loader
字段。不过loading组件、加载失败组件等参数不会有值,只有delay
延时时间默认给了200。
接着就是定义了load
函数用于加载异步组件,这个函数是在第三部分的defineComponent
中调用的,所以我们先来讲defineComponent
函数部分。
第三部分:返回defineComponent定义的组件
我们来看看defineAsyncComponent
的返回值,是一个defineComponent
定义的组件,代码如下:
function defineAsyncComponent(source) {
// ...省略
return defineComponent({
name: "AsyncComponentWrapper",
setup() {
const instance = currentInstance;
const loaded = ref(false);
const error = ref();
const delayed = ref(!!delay);
if (delay) {
setTimeout(() => {
delayed.value = false;
}, delay);
}
load()
.then(() => {
loaded.value = true;
})
.catch((err) => {
onError(err);
error.value = err;
});
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
},
});
}
defineComponent
函数的接收的参数是一个vue组件对象,返回值也是一个vue组件对象。他其实没有做什么事情,单纯的只是提供ts的类型推导。
我们接着来看vue组件对象,对象中只有两个字段:name
属性和setup
函数。
name
属性大家都很熟悉,表示当前vue组件的名称。
大家平时<script setup>
语法糖用的比较多,这个语法糖经过编译后就是setup
函数,当然vue也支持让我们自己手写setup
函数。
提个问题:setup
函数对应的是<script setup>
,我们平时写代码都有template模块对应的是视图部分,也就是熟悉的render函数。为什么这里没有render函数呢?
给setup
函数打个断点,当渲染异步组件时会去执行这个setup
函数。代码将会停留在setup
函数的断点处。
在setup
函数中首先使用ref
定义了三个响应式变量:loaded
、error
、delayed
。
loaded
是一个布尔值,作用是记录异步组件是否加载完成。error
记录的是加载失败时记录的错误信息,如果同时传入了errorComponent
组件,在加载异步组件失败时就会显示errorComponent
组件。delayed
也是一个布尔值,由于loading组件不是立马就显示的,而是延时一段时间后再显示。这个delayed
布尔值记录的是是当前是否还在延时阶段,如果是延时阶段那么就不显示loading组件。
接下来判断传入的参数中设置设置了delay
延迟,如果是就使用setTimeout
延时delay
毫秒才将delayed
的值设置为false,当delayed
的值为false后,在loading阶段才会去显示loading组件。代码如下:
if (delay) {
setTimeout(() => {
delayed.value = false;
}, delay);
}
接下来就是执行load
函数,这个load
函数就是我们前面说的defineAsyncComponent
函数中的第二部分代码。代码如下:
load()
.then(() => {
loaded.value = true;
})
.catch((err) => {
onError(err);
error.value = err;
});
从上面的代码可以看到load
函数明显返回的是一个Promise,所以才可以在后面使用.then()
和.catch()
。并且这里在.then()
中将loaded
的值设置为true,将断点走进load
函数,代码如下:
const load = () => {
return loader()
.catch(() => {
// ...省略
})
.then((comp) => {
if (
comp &&
(comp.__esModule || comp[Symbol.toStringTag] === "Module")
) {
comp = comp.default;
}
resolvedComp = comp;
return comp;
});
};
这里的load
函数代码也很简单,在里面直接执行loader
函数。还记得这个loader
函数是什么吗?
defineAsyncComponent
函数可以接收一个异步加载函数,这个异步加载函数可以在运行时去import导入组件。这个异步加载函数就是这里的loader
函数,执行loader
函数就会去加载异步组件。在我们这里是异步加载async-child.vue
组件,代码如下:
const AsyncChild = defineAsyncComponent(() => import("./async-child.vue"));
所以这里执行loader
函数就是在执行() => import("./async-child.vue")
,执行了import()
后就可以在network
面板看到加载async-child.vue
文件的网络请求。import()
返回的是一个Promise,等import的文件加载完了后就会触发Promise的then()
,所以这里的then()
在此时不会触发。
接着将断点走出load
函数回到setup
函数的最后一个return部分,代码如下:
setup() {
// ...省略
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
},
注意看,这里的setup
的返回值是一个函数,不是我们经常看见的对象。由于这里返回的是函数,此时代码将不会走到返回的函数里面去,给return的函数打个断点。我们暂时先不看函数中的内容,让断点走出setup
函数。发现setup
函数是由vue中的setupStatefulComponent
函数调用的,在我们这个场景中简化后的setupStatefulComponent
函数代码如下:
function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
const setupResult = callWithErrorHandling(setup, instance, 0, [
instance.props,
setupContext,
]);
handleSetupResult(instance, setupResult);
}
上面的callWithErrorHandling
函数从名字你应该就能看出来,调用一个函数并且进行错误处理。在这里就是调用setup
函数,然后将调用setup
函数的返回值丢给handleSetupResult
函数处理。
将断点走进handleSetupResult
函数,在我们这个场景中handleSetupResult
函数简化后的代码如下:
function handleSetupResult(instance, setupResult) {
if (isFunction(setupResult)) {
instance.render = setupResult;
}
}
在前面我们讲过了我们这个场景setup
函数的返回值是一个函数,所以isFunction(setupResult)
的值为true。代码将会走到instance.render = setupResult
,这里的instance
是当前vue组件实例,执行这个后就会将setupResult
赋值给render
函数。
我们知道render函数一般是由template模块编译而来的,执行render函数就会生成虚拟DOM,最后由虚拟DOM生成对应的真实DOM。
当setup
的返回值是一个函数时,这个函数就会作为组件的render函数。这也就是为什么前面defineComponent
中只有name
熟悉和setup
函数,却没有render
函数。
在执行render函数生成虚拟DOM时就会去执行setup
返回的函数,由于我们前面给返回的函数打了一个断点,所以代码将会停留在setup
返回的函数中。回顾一下setup
返回的函数代码如下:
setup() {
// ...省略
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
},
由于此时还没将异步组件加载完,所以loaded
的值也是false,此时代码不会走进第一个if
中。
同样的组件都还没加载完也不会有error,代码也不会走到第一个else if
中。
如果我们传入了loading组件,此时代码也不会走到第二个else if
中。因为此时的delayed
的值还是true,代表还在延时阶段。只有等到前面setTimeout
的回调执行后才会将delayed
的值设置为false。
并且由于delayed
是一个ref响应式变量,所以在setTimeout
的回调中改变了delayed
的值就会重新渲染,也就是再次执行render函数。前面讲了这里的render函数就是setup
中返回的函数,代码就会重新走到第二个else if
中。
此时else if (loadingComponent && !delayed.value)
,其中的loadingComponent
是loading组件,并且delayed.value
的值也是false了。代码就会走到createVNode(loadingComponent)
中,执行这个函数就会将loading组件渲染到页面上。
加载异步组件
前面我们讲过了在渲染异步组件时会执行load
函数,在里面其实就是执行() => import("./async-child.vue")
加载异步组件async-child.vue
,我们也可以在network面板中看到多了一个async-child.vue
文件的请求。
我们知道import()
的返回值是一个Promise,当文件加载完成后就会触发Promise的then()
。此时代码将会走到第一个then()
中,回忆一下代码:
const load = () => {
return loader()
.catch(() => {
// ...省略
})
.then((comp) => {
if (
comp &&
(comp.__esModule || comp[Symbol.toStringTag] === "Module")
) {
comp = comp.default;
}
resolvedComp = comp;
return comp;
});
};
在then()
中判断加载进来的文件是不是一个es6的模块,如果是就将模块的default
导出重写到comp
组件对象中。并且将加载进来的vue组件对象赋值给resolvedComp
变量。
执行完第一个then()
后代码将会走到第二个then()
中,回忆一下代码:
load()
.then(() => {
loaded.value = true;
})
第二个then()
代码很简单,将loaded
变量的值设置为true,也就是标明已经将异步组件加载完啦。由于loaded
是一个响应式变量,改变他的值就会导致页面重新渲染,将会再次执行render函数。前面我们讲了这里的render函数就是setup
中返回的函数,代码就会重新走到第二个else if
中。
再来回顾一下setup
中返回的函数,代码如下:
setup() {
// ...省略
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance);
} else if (error.value && errorComponent) {
return createVNode(errorComponent, {
error: error.value,
});
} else if (loadingComponent && !delayed.value) {
return createVNode(loadingComponent);
}
};
},
由于此时loaded
的值为true,并且resolvedComp
的值为异步加载vue组件对象,所以这次render函数返回的虚拟DOM将是createInnerComp(resolvedComp, instance)
的执行结果。
createInnerComp函数
接着将断点走进createInnerComp
函数,在我们这个场景中简化后的代码如下:
function createInnerComp(comp, parent) {
const { ref: ref2, props, children } = parent.vnode;
const vnode = createVNode(comp, props, children);
vnode.ref = ref2;
return vnode;
}
createInnerComp
函数接收两个参数,第一个参数为要异步加载的vue组件对象。第二个参数为使用defineAsyncComponent
创建的vue组件对应的vue实例。
然后就是执行createVNode
函数,这个函数大家可能有所耳闻,vue提供的h()
函数其实就是调用的createVNode
函数。
在我们这里createVNode
函数接收的第一个参数为子组件对象,第二个参数为要传给子组件的props,第三个参数为要传给子组件的children。createVNode
函数会根据这三个参数生成对应的异步组件的虚拟DOM,将生成的异步组件的虚拟DOM进行return返回,最后就是根据虚拟DOM生成真实DOM将异步组件渲染到页面上。如下图(图后还有一个总结):
总结
本文讲了defineAsyncComponent
是如何实现异步组件的:
在
defineAsyncComponent
函数中会返回一个vue组件对象,对象中只有name
属性和setup
函数。当渲染异步组件时会执行
setup
函数,在setup
函数中会执行内置的一个load
方法。在load
方法中会去执行由defineAsyncComponent
定义的异步组件加载函数,这个加载函数的返回值是一个Promise,异步组件加载完成后就会触发Promise的then()
。在
setup
函数中会返回一个函数,这个函数将会是组件的render函数。当异步组件加载完了后会走到前面说的Promise的
then()
方法中,在里面会将loaded
响应式变量的值修改为true。修改了响应式变量的值导致页面重新渲染,然后执行render函数。前面讲过了此时的render函数是
setup
函数中会返回的回调函数。执行这个回调函数会调用createInnerComp
函数生成异步组件的虚拟DOM,最后就是根据虚拟DOM生成真实DOM,从而将异步子组件渲染到页面上。
关注公众号:【前端欧阳】,给自己一个进阶vue的机会
另外欧阳写了一本开源电子书vue3编译原理揭秘,这本书初中级前端能看懂。完全免费,只求一个star。
vue3的defineAsyncComponent是如何实现异步组件的呢?的更多相关文章
- vue3.x异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块 vue2.x 曾经简单的异步组件 components: { AsyncComponent: () =& ...
- vue3 + vite实现异步组件和路由懒加载
在 Vue2 中,异步组件和路由懒加载处理使用 import 就可以很轻松实现.但是在Vue 3.x 中异步组件的使用与 Vue 2.x 完全不同了.本文就详细讲讲vue3中异步组件和路由懒加载的实现 ...
- vue3的Async Components异步组件
前言: 当我们的项目达到一定的规模时,对于某些组件来说,我们并不希望一开始全部加载,而是需要的时候进行加载:这样的做得目的可以很好的提高用户体验. 传统方式引入组件如下,这样会一次先加载所以组件 先在 ...
- vue的异步组件
异步组件 异步组件:可以在首页加载之前先加载的组件,主要是做性能优化,提高用户体验 一.基本用法 在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件.Vue 提供了 de ...
- React(17)异步组件
26.异步组件当在React里使用异步组件时,核心知识是两个: webpack 如何异步加载其他模块:通过 require(['xxx'], function(module){})来实现:React ...
- 026-微软Ajax异步组件
ASP.Net中内置的简化AJAX开发的控件UpdatePanel放入ScriptManager,将要实现AJAX效果的控件放到UpdatePanel中即可.UpdatePanel原理探秘,用Http ...
- vue路由异步组件案例
最近研究了vue性能优化,涉及到vue异步组件.一番研究得出如下的解决方案. 原理:利用webpack对代码进行分割是异步调用组件前提.异步组件在优先级上让位同步组件.下面介绍的是怎么实现异步组件. ...
- vue的异步组件按需加载
当build打包后,app.js过大的时候,可以考虑用异步组件的方式. import HomeHeader from "./components/Header"; import H ...
- 七、vue语法补充二(动态组件 & 异步组件、访问元素 & 组件、混入)
1..sync 修饰符 2.3.0+ 新增 vue 修饰符sync的功能是:当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定.类似于v-model的效果 例子: this.$ ...
- vue项目实现按需加载的3种方式:vue异步组件技术、es提案的import()、webpack提供的require.ensure()
1. vue异步组件技术 vue-router配置路由,使用vue的异步组件技术,可以实现按需加载. 但是,这种情况下一个组件生成一个js文件. 举例如下: { path: '/promisedemo ...
随机推荐
- python globals()[]将字符串转化类,并通过反射执行方法
背景: 通过关键字设计ui自动化框架,将测试用例及其步骤存放到excel文件:其中步骤中包含了封装好的关键字方法,如打开浏览器.输入页面操作等,关键字保存的内容:具体类实例.方法 通过excel获取到 ...
- Kubernetes(二)资源管理
1. 资源管理介绍 在kubernetes中,所有内容都抽象为资源,用户需要操作资源来管理kubernetes. Kubernetes本质上就是一个集群系统,用户可以在集群中部署各种服务,所谓的部署服 ...
- Hugging Face Accelerate 两个后端的故事:FSDP 与 DeepSpeed
社区中有两个流行的 零冗余优化器(Zero Redundancy Optimizer,ZeRO) 算法实现,一个来自 DeepSpeed,另一个来自 PyTorch.Hugging Face Acce ...
- Linux设备模型:2、基本对象 Kobject、Kset、Ktype
原文:http://www.wowotech.net/device_model/kobject.html 作者:wowo 发布于:2014-3-7 0:25 分类:统一设备模型 前言 Kobject是 ...
- 关于ZYNQ-7000中断调试一点感想
背景 在ZYNQ 平台下,需要对各种需要的底层接口进行初始化. 我依次调试了很多驱动,从最简单的网口到USB:再到读写PL端的寄存器(通过AXI总线,内存映射读写物理地址实现),到中断的时候一直卡着不 ...
- 关于docker-compose up -d 出现超时情况处理
由于要搭建一个ctf平台,用docker一键搭建是出现超时情况 用了很多办法,换源,等之类的一样没办法,似乎它就是只能用官方那个一样很怪. 只能用一种笨办法来处理了,一个个pull. 打个比如: 打开 ...
- 【Grafana】Grafana模板自定义-1-创建选择框
如何创建选择框 第一步:编辑模板 第二步:配置变量 配置说明: General: [Name]变量名,后面模板中如果要按条件筛选,会用到这个变量名. [Type]类型,目前没仔细研究,使用默认的Que ...
- 2个qubit的量子门
量子计算机就是基于单qubit门和双qubit门的,再多的量子操作都是基于这两种门.双qubit门比单qubit门难理解得多,不过也重要得多.它可以用来创建纠缠,没有纠缠,量子机就不可能有量子霸权. ...
- Linux 命令指南
做这个东西有两个用处,一是初赛会考,二是考场上用 windows 哪里数组越界你都不知道直接 RE 爆炸. sudo -s 输入后填写密码获得管理员权限. cd 打开文件或者目录,用法是 cd 目录名 ...
- 记一次Redis实施故障
服务在测试环境运行没问题,部署到生产环境,连redis时报下面的错误: [2022-11-04 00:00:09][org.springframework.scheduling.support.Tas ...