臥槽,牛逼!vue3的元件竟然還能“暫停”渲染!

前端欧阳發表於2024-08-19

前言

有的時候我們想要從服務端拿到資料後再去渲染一個元件,為了實現這個效果我們目前有幾種實現方式:

  • 將資料請求放到父元件去做,並且使用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。

相關文章