有沒有一種完美的方案,從服務端獲取數據的邏輯放在子組件中,並且在獲取數據的期間讓子組件“暫停”一下,先不去渲染,等到數據請求完成後再第一次去渲染子組件呢? ...
前言
有的時候我們想要從服務端拿到數據後
再去渲染一個組件,為了實現這個效果我們目前有幾種實現方式:
-
將數據請求放到父組件去做,並且使用
v-if
控制拿到子組件後才去渲染子組件,然後將數據從父組件通過props
傳給子組件。 -
在子組件的
onMounted
中請求數據,並且使用v-if
在子組件的template
最外層進行控制,只有拿到數據後才渲染子組件中的內容。
上面這兩種方案都有各自的缺點,不夠完美。最理想的方案是將從服務端獲取數據的邏輯放在子組件中,並且在獲取數據的期間讓子組件“暫停”
一下,先不去渲染,等到數據請求完成後再第一次去渲染子組件。
歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。
完美的解決方案
第一種方法的缺點是:子組件雖然拿到數據後才開始渲染,但是數據請求的邏輯卻放到了父組件上面,我們期望所有的邏輯都封裝在子組件內部。
第二種方法的缺點是:實際上是初始化時就渲染了一次子組件,此時我們還沒從服務端拿到數據。所以不得不使用v-if
在template
的最外層控制,此時不渲染子組件中的內容。當從服務端拿到數據後再第二次渲染子組件,此時才將子組件中的內容渲染到頁面上。這種方法明顯子組件渲染了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
的邏輯全部放到了父組件中,並且使用props
將user
傳遞給子組件,並且在從服務端獲取數據的期間顯示一個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中的內容。
下麵這個是我畫的流程圖(流程圖後面還有文末總結):
總結
這篇文章我們講了有的場景需要從服務端拿到數據後
再去渲染一個組件,此時我們就可以使用父組件Suspense + 子組件頂層await
的完美方案。
在渲染父組件的Suspense
組件時發現他的子組件有非同步組件,就會“暫停”渲染子組件,改為自動渲染loading組件。
子組件在setup
頂層使用await
等待從服務端請求數據,當從服務端拿到了數據後此時子組件才算是載入完成,此時才會進行第一次渲染,並且自動將loading中的內容替換為子組件中渲染的內容。
並且在Suspense
中還支持多個非同步子組件分別從服務端獲取數據,等這幾個非同步子組件都從服務端獲取到數據後才會自動的將loading替換為這幾個非同步子組件渲染的內容。
最後就是Suspense
組件目前依然還是實驗性
的功能,生產環境使用需要謹慎。
關註公眾號:【前端歐陽】,給自己一個進階vue的機會
另外歐陽寫了一本開源電子書vue3編譯原理揭秘,看完這本書可以讓你對vue編譯的認知有質的提升。這本書初、中級前端能看懂,完全免費,只求一個star。