Skip to content
⚠️ This article was written in 2020. Some content may be outdated.

Vue 3 Teleport、Fragment、Suspense 新特性

Vue 3 引入了三個實用的內置組件:Teleport 將子節點渲染到 DOM 樹的任意位置,Fragment 支持多根節點模板,Suspense 處理異步組件的加載態。這三個特性解決了 Vue 2 中長期存在的痛點。

Teleport:跳出組件層級

Modal、Tooltip、全屏 Loading 這類組件經常因為父級的 overflow: hiddenz-index 被遮擋。Teleport 可以將 DOM 移動到指定位置,同時保持組件的邏輯歸屬不變。

vue
<template>
  <div>
    <button @click="showModal = true">打開彈窗</button>
    <!-- 渲染到 body 末尾,不受父組件 CSS 影響 -->
    <teleport to="body">
      <div v-if="showModal" class="modal-overlay">
        <div class="modal-content">
          <h3>確認操作</h3>
          <p>確定要刪除這條記錄嗎?</p>
          <button @click="confirmDelete">確定</button>
          <button @click="showModal = false">取消</button>
        </div>
      </div>
    </teleport>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const showModal = ref(false)
    const confirmDelete = () => {
      console.log('已刪除')
      showModal.value = false
    }
    return { showModal, confirmDelete }
  }
}
</script>

關鍵點:Teleport 的 to 接受任何 CSS 選擇器。組件卸載時,Teleport 的內容也會自動移除。

Fragment:告別多餘包裹節點

Vue 2 要求模板只有一個根節點,這導致大量無意義的 <div> 包裹層。Vue 3 支持多根節點。

vue
<template>
  <!-- Vue 2 必須包裹一層 div -->
  <!-- Vue 3 可以直接多個根節點 -->
  <header>
    <nav>
      <a href="/">首頁</a>
      <a href="/about">關於</a>
    </nav>
  </header>
  <main>
    <slot />
  </main>
  <footer>
    <p>&copy; 2020 Vue Blog</p>
  </footer>
</template>

<script>
export default {
  name: 'Layout'
}
</script>

注意:使用多根節點時,不能通過 $attrs 直接透傳屬性到根元素,需要顯式綁定 v-bind="$attrs" 到某個具體元素上。

Suspense:異步組件的加載態

Suspense 可以等待嵌套的異步依賴(異步組件或組件內的異步 setup)全部就緒後再渲染。

vue
<template>
  <Suspense>
    <template #default>
      <UserProfile :userId="1" />
    </template>
    <template #fallback>
      <div class="loading-skeleton">
        <div class="skeleton-avatar"></div>
        <div class="skeleton-text"></div>
        <div class="skeleton-text short"></div>
      </div>
    </template>
  </Suspense>
</template>

<script>
import { defineAsyncComponent } from 'vue'

const UserProfile = defineAsyncComponent({
  loader: () => import('./components/UserProfile.vue'),
  loadingComponent: {
    template: '<div>加載中...</div>'
  },
  delay: 200,
  timeout: 10000
})

export default {
  components: { UserProfile }
}
</script>

配合 setup 中的異步操作,Suspense 可以等待 setup 函數返回 Promise。

vue
<script>
import { ref } from 'vue'

export default {
  async setup() {
    const data = ref(null)
    // setup 返回 Promise,Suspense 會等待它完成
    const res = await fetch('/api/user/profile')
    data.value = await res.json()
    return { data }
  }
}
</script>

三個特性的組合使用

實際項目中這三個特性經常配合使用。比如一個異步加載的全局 Dialog:

vue
<template>
  <Teleport to="body">
    <Suspense>
      <template #default>
        <AsyncDialog v-if="visible" @close="visible = false" />
      </template>
      <template #fallback>
        <div class="dialog-loading">加載彈窗組件中...</div>
      </template>
    </Suspense>
  </Teleport>
</template>

<script>
import { defineAsyncComponent, ref } from 'vue'

const AsyncDialog = defineAsyncComponent(() =>
  import('./HeavyDialog.vue')
)

export default {
  components: { AsyncDialog },
  props: { visible: Boolean }
}
</script>

小結

  • Teleport 解決了 Modal/Tooltip 被父級 CSS 影響的經典問題,組件邏輯不移動,只移動 DOM
  • Fragment 消除了無意義的包裹節點,但要注意 $attrs 顯式綁定
  • Suspense 統一管理異步組件和異步 setup 的加載態,替代手動 loading 狀態
  • 三個特性可以自由組合,構建更靈活的組件結構

MIT Licensed