vue3的defineAsyncComponent是如何实现异步组件的呢?

news/2024/11/17 12:01:45/文章来源:https://www.cnblogs.com/heavenYJJ/p/18355747

前言

在上一篇 给我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图:
demo

从上面的gif图可以看到,当我们点击load async child按钮后,在network面板中才会去加载异步组件async-child.vue

defineAsyncComponent除了像上面这样直接接收一个返回Promise的回调函数之外,还可以接收一个对象作为参数。demo代码如下:

const AsyncComp = defineAsyncComponent({// 加载函数loader: () => import('./async-child.vue'),// 加载异步组件时使用的组件loadingComponent: LoadingComponent,// 展示加载组件前的延迟时间,默认为 200msdelay: 200,// 加载失败后展示的组件errorComponent: ErrorComponent,// 如果提供了一个 timeout 时间限制,并超时了// 也会显示这里配置的报错组件,默认值是:Infinitytimeout: 3000
})

其中对象参数有几个字段:

  • loader字段其实对应的就是前面那种写法中的回调函数。

  • loadingComponent为加载异步组件期间要显示的loading组件。

  • delay为显示loading组件的延迟时间,默认200ms。这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。

  • errorComponent为加载失败后显示的组件。

  • timeout为超时时间。

在接下来的源码分析中,我们还是以前面那个接收一个返回Promise的回调函数为例子进行debug调试源码。

开始打断点

我们在浏览器中接着来看父组件index.vue编译后的代码,很简单,在浏览器中可以像vscode一样使用command(windows中是control)+p就可以唤起一个输入框,然后在输入框中输入index.vue点击回车就可以在source面板中打开编译后的index.vue文件了。如下图:
command

我们看到编译后的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函数接收的参数可以是一个回调函数,也可以是包含loaderloadingComponenterrorComponent等字段的对象。因为如果我们传入的是回调函数,在内部会将传入的回调函数赋值给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定义了三个响应式变量:loadederrordelayed

  • 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将异步组件渲染到页面上。如下图(图后还有一个总结):
progress

总结

本文讲了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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/783133.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从自建到云原生:数据管理的未来与变革

在数据技术不断演进的背景下,云数据库的崛起和云原生数据库的普及标志着数据库技术的显著变革。从最初的自建数据库模式到如今的云原生数据库,企业在数据管理上的选择变得更加丰富和灵活。云数据库不仅仅是对传统数据库技术的一个迁移,更是对其进行了一次全面的升级和优化。…

P9520 [JOISC2022] 监狱

P9520 [JOISC2022] 监狱 题目描述 有一棵 \(N\) 个节点的树,有 \(M\) 个囚犯,要从 \(S_i\) 走到 \(T_i\)。每一时刻可以发布一个命令让一名囚犯走到相邻的节点,要求任意时刻囚犯不能走到同一个节点上,求是否可以令每一个囚犯从 \(S_i\) 走到 \(T_i\)。 做法解析 首先我们可…

《软件性能测试分析与调优实践之路》(第2版) 读书笔记(一)总体介绍(上)-真正从性能分析与调优来看性能测试

《软件性能测试分析与调优实践之路》(第2版) 是清华大学出版社出版的一本图书,作者为张永清,全书共分为9章,如下图所示 图书介绍:《软件性能测试分析与调优实践之路》(第2版) 1、为什么需要性能测试与分析 1)、了解系统的各项性能指标,通过性能压测来了解系统能承受多大…

人人都能搞定的大模型原理 - 神经网络

​人工智能的发展起步于1950年,期间经历了各种里程碑和变革,与此相关的神经网络技术也从最初的单层感知到复杂的层级和卷积神经网络一路创新和变革,不断推动人工智能领域的发展,直到 2022 年 ChatGPT 的问世,彻底引爆了大众的目光。 人工智能技术经历了漫长的迭代过程,无…

SpringBoot优雅开发REST API最佳实践

接口服务主要由两部分组成,即参数(输入)部分,响应(输出)部分。其中在SpringBoot中主要是Controller层作为API的开发处,其实在架构层面来讲,Controller本身是一个最高的应用层,它的职责是调用、组装下层的interface服务数据,核心是组装和调用,不应该掺杂其他相关的逻…

开源的 P2P 跨平台传文件应用「GitHub 热点速览」

起初,我以为这是“微软菩萨”降临,但玩了一圈下来,发现实际效果并没有那么惊艳,还没上周热门的开源项目有趣。例如,基于 WebRTC 的文件传输平台 ShareDrop,只需打开网页,就能在局域网或互联网上安全地跨设备传文件。而可自建支持目标检测和安全报警的视频监控平台 Friga…

删除个文件夹,vfs2上传文件到ftp就异常553,这么不经事吗

开心一刻 今天逛街碰到街头采访,一上来就问我敏感话题 主持人:小哥哥,你单身吗 我:是啊 主持人:你找女朋友的话,是想找一个小奶猫呢,还是小野猫呢 我沉思了一下,叹气道:如果可以的话,我想找个人,而且是女人上传文件 基于 commons-vfs2 实现文件到 FTP 服务器的上传,…

8.13信息学集训_数据结构

目录P1981 [NOIP2013 普及组] 表达式求值P1449 后缀表达式P5788 【模板】单调栈P1886 滑动窗口 /【模板】单调队列P1901 发射站P1540 [NOIP2010 提高组] 机器翻译P2085 最小函数值P6033 [NOIP2004 提高组] 合并果子 加强版P1631 序列合并P2251 质量检测P1305 新二叉树B3631 单向…

Windows Server 2025 中文版、英文版下载 (Inside Preview, updated Aug 2024) - 下一代 Windows 11 Server

Windows Server 2025 中文版、英文版下载 (Inside Preview, updated Aug 2024) - 下一代 Windows 11 ServerWindows Server 2025 中文版、英文版下载 (Inside Preview, updated Aug 2024) - 下一代 Windows 11 Server Windows Server 2025 正式版发布在即 请访问原文链接:http…

如何让您的 .NET应用程序更智能-- 请参加 8.20 的 .NET Conf--Focus on AI

Microsoft 将于 2024 年 8 月 20 日举办免费的 .NET Conf: Focus on AI。该虚拟活动为开发人员提供了如何集成 .NET 和 AI 以增强应用程序开发和用户体验的见解,其中包括专家和行业领导者的会议。 为什么你不应该错过这个活动? .NET Conf: Focus 系列由全年举行的小型现场活…

如何让您的 .NET应用程序更智能-- 请参加 8.20 的 .NET Conf ndash; Focus on AI

Microsoft 将于 2024 年 8 月 20 日举办免费的 .NET Conf: Focus on AI。该虚拟活动为开发人员提供了如何集成 .NET 和 AI 以增强应用程序开发和用户体验的见解,其中包括专家和行业领导者的会议。为什么你不应该错过这个活动?.NET Conf: Focus 系列由全年举行的小型现场活动…

如何让您的 .NET应用程序更智能-- 请参加 8.20 的 .NET Conf -- Focus on AI

Microsoft 将于 2024 年 8 月 20 日举办免费的 .NET Conf: Focus on AI。该虚拟活动为开发人员提供了如何集成 .NET 和 AI 以增强应用程序开发和用户体验的见解,其中包括专家和行业领导者的会议。为什么要参加?.NET Conf: Focus 系列由全年举行的小型现场活动组成。8 月 20…