有的时候我们想要接口拿到数据后再去渲染一个组件,为了实现这个效果我们目前有两种实现方式:
-
将数据请求放到父组件去做,并且使用
v-if控制拿到子组件后才去渲染子组件,然后将数据从父组件通过props传给 -
在子组件的
onMounted中请求数据,并且使用v-if在子组件的template最外层进行控制,只有拿到数据后才渲染子组件中的内容。
上面这两种方案都有各自的缺点,不够完美。最理想的方案是将从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件“暂停”一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件。
-
第一种方法的缺点是:子组件虽然拿到数据后才开始渲染,但是数据请求的逻辑却放到了父组件上面,我们期望所有的逻辑都封装在子组件内部。
-
第二种方法的缺点是:实际上是初始化时就渲染了一次子组件,此时我们还没从服务端拿到数据。所以不得不使用
v-if在template的最外层控制,此时不渲染子组件中的内容。当从服务端拿到数据后再第二次渲染子组件,此时才将子组件中的内容渲染到页面上。这种方法明显子组件渲染了2次。
vue3提供的完美解决方案
从服务端获取数据的逻辑放在子组件中,并且在获取数据的期间让子组件“暂停”一下,先不去渲染,等到数据请求完成后再第一次去渲染子组件呢?
最完美的方案就是在 服务端请求期间让子组件“暂停”渲染, 让父组件展示一个loading页面。并且这个loading的显示逻辑不需要封装在子组件中,在“暂停”渲染期间自动就能显示出来。等到从服务端请求
<template>
<Suspense>
<AsyncChildDemo />
<template #fallback>loading...</template>
</Suspense>
</template>
<script setup lang="ts">
import AsyncChildDemo from "./AsyncChild.vue";
</script>
下面这个是使用了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() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
name: "qqq",
phone: "19999999999",
});
}, 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在渲染子组件时,发现子组件是一个异步组件就不会立即执行异步子组件的render函数。而是会加一个名为deps的标记,标明当前默认子组件是一个异步组件,暂停渲染异步子组件。
由于异步子组件是一个Promise,所以可以在加载异步子组件的Promise后添加.then()方法,在.then()方法中才会去继续渲染异步子组件。
目前异步子组件已经暂停渲染了,接着就是会去读取deps标记。如果deps标记为true,说明异步子组件暂停渲染了,此时就会去将fallback插槽中的loading组件渲染到页面上。
当异步子组件加载完成后就会触发Promise的.then()方法,从而继续渲染异步子组件。在.then()方法中会去执行异步子组件的render函数去生成虚拟DOM,然后根据虚拟DOM生成真实DOM。最后就是将原本页面上渲染的fallback插槽中的内容替换为异步组件生成的真实DOM中的内容。
<template>
<div v-if="deps.length > 0">
<slot />
</div>
<div v-else>
<slot name="fallback" />
</div>
</template>
<script>
export default {
name: 'Suspense',
setup() {
const deps = [];
// 假设有一个函数来注册依赖项
const registerDep = (dep) => {
deps.push(dep);
};
// 假设有一个函数来清除所有依赖项
const clearDeps = () => {
deps.length = 0;
};
// 在 onUnmounted 钩子中清除依赖项
onUnmounted(clearDeps);
return { deps, registerDep };
}
};
</script>