首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

卧槽,牛逼!vue3的组件竟然还能“暂停”渲染!

编程知识
2024年08月19日 07:38

前言

有的时候我们想要从服务端拿到数据后再去渲染一个组件,为了实现这个效果我们目前有几种实现方式:

  • 将数据请求放到父组件去做,并且使用v-if控制拿到子组件后才去渲染子组件,然后将数据从父组件通过props传给子组件。

  • 在子组件的onMounted中请求数据,并且使用v-if在子组件的template最外层进行控制,只有拿到数据后才渲染子组件中的内容。

上面这两种方案都有各自的缺点,不够完美。最理想的方案是将从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件“暂停”一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件。

欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

完美的解决方案

第一种方法的缺点是:子组件虽然拿到数据后才开始渲染,但是数据请求的逻辑却放到了父组件上面,我们期望所有的逻辑都封装在子组件内部。

第二种方法的缺点是:实际上是初始化时就渲染了一次子组件,此时我们还没从服务端拿到数据。所以不得不使用v-iftemplate的最外层控制,此时不渲染子组件中的内容。当从服务端拿到数据后再第二次渲染子组件,此时才将子组件中的内容渲染到页面上。这种方法明显子组件渲染了2次。

那么有没有一种完美的方案,从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件“暂停”一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件呢?

答案是:当然可以,vue3的Suspense组件+在setup顶层使用await获取数据就能完美的实现这个需求!!!

两个不完美的例子

为了让你更直观的看到完美方案的牛逼,我们先来看看前面讲的两个不够完美的例子。

父组件中请求数据的例子

下面这个是父组件中请求数据的例子,父组件的代码如下:

<template>
  <ChildDemo v-if="user" :user="user" />
  <div v-else>
    <p>loading...</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";
import ChildDemo from "./Child.vue";

const user = ref(null);

async function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}

onMounted(async () => {
  user.value = await fetchUser();
});
</script>

子组件的代码如下:

<template>
  <div>
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
</template>

<script setup lang="ts">
const props = defineProps(["user"]);
</script>

这种方案我们将从服务端获取user的逻辑全部放到了父组件中,并且使用propsuser传递给子组件,并且在从服务端获取数据的期间显示一个loading的文案。

这样虽然实现了我们的需求但是将子组件获取user的逻辑放到了父组件中,我们期望将这些逻辑全部封装在子组件中,所以这个方案并不完美。

子组件在onMounted中请求数据的例子

我们来看看第二种方案,父组件代码代码如下:

<template>
  <ChildDemo />
</template>

<script setup lang="ts">
import ChildDemo from "./Child.vue";
</script>

子组件代码如下:

<template>
  <div v-if="user">
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
  <div v-else>
    <p>loading...</p>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from "vue";

const user = ref(null);

async function fetchUser() {
  // 使用setTimeout模拟从服务端获取数据
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}

onMounted(async () => {
  user.value = await fetchUser();
});
</script>

我们将数据请求放在了onMounted中,初始化时会去第一次渲染子组件。此时user的值还是null,所以我们不得不在template的最外层使用v-if="user"控制此时不显示子组件的内容,在v-else中去渲染loading文案。

当从服务端拿到数据后给响应式变量user重新赋值,会触发页面重新渲染,此时会进行第二次渲染才将子组件的内容渲染到页面上。

从上面可以看到这种方案子组件明显渲染了两次,并且我们还将loading的显示逻辑写在子组件的内部,增加了子组件代码的复杂度。所以这种方案也并不完美。

最完美的方案就是在fetchUser期间让子组件“暂停”渲染fallback去渲染一个loading页面。并且这个loading的显示逻辑不需要封装在子组件中,在“暂停”渲染期间自动就能显示出来。等到从服务端请求数据完成后才开始渲染子组件,并且自动的卸载掉loading页面。

Suspense + await实现完美的例子

下面这个是官网对Suspense的介绍:

<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。

上面的意思是Suspense组件能够监听下面的异步子组件,在等待异步子组件完成渲染之前,可以去渲染一个loading的页面。

Suspense组件支持两个插槽:#default 和 #fallback。如果#default插槽中有异步组件,那么就会先去渲染 #fallback中的内容,等到异步组件加载完成后就会将#fallback中的内容给干掉,改为将异步组件的内容渲染到页面上。

如果我们的子组件是一个异步组件,那么Suspense不就可以帮我们实现想要的功能吖。

Suspense可以在异步子组件的加载过程中使用 #fallback插槽自动帮我们渲染一个加载中的loading,等到异步子组件加载完成后才会第一次去渲染子组件中的内容。

那么现在的问题是如何将我们的子组件变成异步子组件?

这个问题的答案其实vue官网就已经告诉我们了,如果一个组件的<script setup>顶层使用了await,那么这个组件就会变成一个异步组件。我们接下来只需要在子组件的顶层使用await去请求服务端数据就可以啦。

完美方案的父组件

下面这个是使用Suspense改造后的父组件代码,如下:

<template>
  <Suspense>
    <AsyncChildDemo />
    <template #fallback>loading...</template>
  </Suspense>
</template>

<script setup lang="ts">
import AsyncChildDemo from "./AsyncChild.vue";
</script>

在父组件中使用了Suspense组件,给这个组件传了2个插槽。#default插槽为异步子组件AsyncChildDemo,默认插槽可以不用给元素上面添加#default

并且使用了#fallback插槽,在异步子组件加载过程中会暂时先不去渲染异步子组件AsyncChildDemo。改为先渲染#fallback插槽中的loading,等到异步子组件加载完成后会自动将loading替换为子组件中的内容。

完美方案的子组件

下面这个是使用了await改造后的子组件代码,如下:

<template>
  <div>
    <p>用户名:{{ user.name }}</p>
    <p>手机号:{{ user.phone }}</p>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const user = ref(null);
user.value = await fetchUser();

async function fetchUser() {
  // 使用setTimeout模拟从服务端获取数据
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "张三",
        phone: "13800138000",
      });
    }, 2000);
  });
}
</script>

我们在<script setup>顶层中使用了await,然后将await拿到的值赋值给user变量。在顶层使用了await后子组件就变成了一个异步组件,等到await fetchUser()执行完了后,也就是从服务端拿到了数据后,子组件才算是加载完成了。

并且由于我们在父组件中使用了Suspense,所以在子组件加载完成之前,也就是从服务端拿到数据之前,都不会去渲染子组件(相当于“暂停”渲染子组件)。而是去渲染#fallback插槽中的loading,等到从服务端拿到数据之后异步子组件才算是加载完成了。此时才会第一次去渲染子组件,并且将loading替换为子组件渲染的内容。

因为第一次渲染子组件时已经从服务端拿到了user的值,此时user已经不是null了,所以我们可以不用在template的最上层使用v-if="user",尽管在template中有去读user.name

经过父组件Suspense + 子组件顶层await的改造后,在渲染父组件的Suspense时发现他的子组件有异步组件,就会“暂停”渲染子组件,改为自动渲染loading组件。

子组件在setup顶层使用await等待从服务端请求数据,当从服务端拿到了数据后此时子组件才算是加载完成,此时才会进行第一次渲染,并且自动将loading中的内容替换为子组件中渲染的内容。

并且在Suspense中还支持多个异步子组件分别从服务端获取数据,等这几个异步子组件都从服务端获取到数据后才会自动的将loading替换为这几个异步子组件渲染的内容。

还有就是Suspense组件目前依然还是实验性的功能,生产环境使用需要谨慎。

简单看看Suspense如何实现“暂停”渲染?

Suspense在渲染子组件时,发现子组件是一个异步组件就不会立即执行异步子组件的render函数。而是会加一个名为deps的标记,标明当前默认子组件是一个异步组件,暂停渲染异步子组件。

由于异步子组件是一个Promise,所以可以在加载异步子组件的Promise后添加.then()方法,在.then()方法中才会去继续渲染异步子组件。

目前异步子组件已经暂停渲染了,接着就是会去读取deps标记。如果deps标记为true,说明异步子组件暂停渲染了,此时就会去将fallback插槽中的loading组件渲染到页面上。

当异步子组件加载完成后就会触发Promise.then()方法,从而继续渲染异步子组件。在.then()方法中会去执行异步子组件的render函数去生成虚拟DOM,然后根据虚拟DOM生成真实DOM。最后就是将原本页面上渲染的fallback插槽中的内容替换为异步组件生成的真实DOM中的内容。

下面这个是我画的流程图(流程图后面还有文末总结):
full-progress

总结

这篇文章我们讲了有的场景需要从服务端拿到数据后再去渲染一个组件,此时我们就可以使用父组件Suspense + 子组件顶层await的完美方案。

在渲染父组件的Suspense组件时发现他的子组件有异步组件,就会“暂停”渲染子组件,改为自动渲染loading组件。

子组件在setup顶层使用await等待从服务端请求数据,当从服务端拿到了数据后此时子组件才算是加载完成,此时才会进行第一次渲染,并且自动将loading中的内容替换为子组件中渲染的内容。

并且在Suspense中还支持多个异步子组件分别从服务端获取数据,等这几个异步子组件都从服务端获取到数据后才会自动的将loading替换为这几个异步子组件渲染的内容。

最后就是Suspense组件目前依然还是实验性的功能,生产环境使用需要谨慎。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

From:https://www.cnblogs.com/heavenYJJ/p/18366122
本文地址: http://www.shuzixingkong.net/article/1218
0评论
提交 加载更多评论
其他文章 【团队建设】如何做好团队开发中的 CodeReview(代码评审)?
你是否曾写过一个很简单的需求或者优化?而且你认为不需要审查,就可以直接合并到主分支。可能过了几天或者几周,你突然意识到你犯了一个明显的或是不应该犯的错误,如果有其他人来审查代码,那这个问题可能就会被发现并及时处理。
花了一天时间帮财务朋友开发了一个实用小工具
大家好,我是晓凡。 写在前面 不知道大家有没有做财务的朋友,我就有这么一位朋友就经常跟我抱怨。一到月底简直就是噩梦,总有加不完的班,熬不完的夜,做不完的报表。 一听到这儿,这不就一活生生的一个“大表哥”么,这加班跟我们程序员有得一拼了,忍不住邪恶一笑,心里平衡了很多。 身为牛马,大家都不容易啊。我不
花了一天时间帮财务朋友开发了一个实用小工具 花了一天时间帮财务朋友开发了一个实用小工具 花了一天时间帮财务朋友开发了一个实用小工具
推荐5款免费、开箱即用的Vue后台管理系统模板
前言 在现今的软件开发领域,Vue凭借其高效、灵活和易于上手的特性,成为了前端开发的热门选择。对于需要快速搭建企业级后台管理系统的开发者而言,使用现成的Vue后台管理系统模板无疑是一个明智之举。本文大姚将为你推荐5款开源、免费、开箱即用的Vue后台管理系统模板,帮助你快速启动项目,专注于业务逻辑的开
推荐5款免费、开箱即用的Vue后台管理系统模板 推荐5款免费、开箱即用的Vue后台管理系统模板 推荐5款免费、开箱即用的Vue后台管理系统模板
解密Prompt系列36. Prompt结构化编写和最优化算法UNIPROMPT
这一章我们就重点关注描述性指令优化。我们先简单介绍下结构化Prompt编写,再聊聊从结构化多角度进行Prompt最优化迭代的算法方案UniPrompt
解密Prompt系列36. Prompt结构化编写和最优化算法UNIPROMPT 解密Prompt系列36. Prompt结构化编写和最优化算法UNIPROMPT 解密Prompt系列36. Prompt结构化编写和最优化算法UNIPROMPT
ChatGPT学习之旅 (9) 系统运维小助手
本篇给大家分享下我在日常的系统开发和运维工作中的小任务是如何通过ChatGPT来解决的。可以毫不犹豫地说,它就是我的系统运维小助手。
ChatGPT学习之旅 (9) 系统运维小助手 ChatGPT学习之旅 (9) 系统运维小助手 ChatGPT学习之旅 (9) 系统运维小助手
可以调用Null的实例方法吗?
前几天有个网友问我一个问题:调用实例方法的时候为什么目标对象不能为Null。看似一个简单的问题,还真不是一句话就能说清楚的。而且这个结论也不对,当我们调用定义在某个类型的实例方法时,目标对象其实可以为Null。一、从ECMA-335 Spec说起 二、Call V.S Callvirt 三、直接调用
可以调用Null的实例方法吗? 可以调用Null的实例方法吗?
如何诱导AI犯罪-提示词注入
我们用到的大模型基本把政治类信息、犯罪相关信息都已屏蔽。但是,黑客依旧可以使用提示词诱导和提示词注入的方式对大模型进行攻击。
如何诱导AI犯罪-提示词注入 如何诱导AI犯罪-提示词注入 如何诱导AI犯罪-提示词注入
C#开发的应用升级管理器LUAgent客户端 - 开源研究系列文章 - 个人小作品
以前就想开发应用的升级更新的程序,也想过把升级工具单独弄成类库模块化。后来就想到要开发服务端和客户端独立出来,只要配置好就能够对任何应用进行升级更新操作。 笔者将这个命名为:LUAgent,即Lzhdim Update Agent,升级更新代理,上次发布了服务端工具:https://www.cnbl
C#开发的应用升级管理器LUAgent客户端 - 开源研究系列文章 - 个人小作品 C#开发的应用升级管理器LUAgent客户端 - 开源研究系列文章 - 个人小作品 C#开发的应用升级管理器LUAgent客户端 - 开源研究系列文章 - 个人小作品