Vue3 新特性

Fragment、Teleport、Suspense 及编译优化

问题

Vue3 引入了哪些新特性?包括 Fragment、Teleport、Suspense、Tree-shaking 优化、PatchFlag、HoistStatic。

解答

Fragment

Vue3 组件可以有多个根节点,不再需要单一根元素包裹。

<!-- Vue2 必须有单一根节点 -->
<template>
  <div>
    <header>头部</header>
    <main>内容</main>
  </div>
</template>

<!-- Vue3 支持多根节点 -->
<template>
  <header>头部</header>
  <main>内容</main>
  <footer>底部</footer>
</template>

Teleport

将组件内容渲染到 DOM 中的任意位置,常用于模态框、通知等场景。

<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <!-- 将内容传送到 body 下 -->
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <p>这是一个模态框</p>
      <button @click="showModal = false">关闭</button>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

<style>
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: white;
  padding: 20px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
</style>

Suspense

处理异步组件的加载状态,提供 loading 和 fallback 机制。

<template>
  <Suspense>
    <!-- 异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>
    
    <!-- 加载中显示的内容 -->
    <template #fallback>
      <div>Loading...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

// 异步组件
const AsyncComponent = defineAsyncComponent(() => 
  import('./AsyncComponent.vue')
)
</script>
<!-- AsyncComponent.vue -->
<template>
  <div>{{ data }}</div>
</template>

<script setup>
// 组件内使用 async setup
const response = await fetch('/api/data')
const data = await response.json()
</script>

Tree-shaking 优化

Vue3 采用模块化设计,未使用的 API 会被打包工具移除。

// Vue2 - 全量引入
import Vue from 'vue'
Vue.nextTick(() => {})

// Vue3 - 按需引入,未使用的不会打包
import { ref, computed, watch, nextTick } from 'vue'

// 只用了 ref,其他 API 会被 tree-shake 掉
const count = ref(0)

PatchFlag(静态标记)

编译时标记动态内容,运行时只对比动态部分,跳过静态内容。

<template>
  <div>
    <p>静态文本</p>
    <p>{{ message }}</p>
    <p :class="className">动态 class</p>
  </div>
</template>

编译后的渲染函数:

import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    // 静态节点,无 patchFlag
    _createElementVNode("p", null, "静态文本"),
    
    // patchFlag = 1,表示动态文本
    _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    
    // patchFlag = 2,表示动态 class
    _createElementVNode("p", {
      class: _normalizeClass(_ctx.className)
    }, "动态 class", 2 /* CLASS */)
  ]))
}

常见 PatchFlag 值:

export const enum PatchFlags {
  TEXT = 1,         // 动态文本
  CLASS = 2,        // 动态 class
  STYLE = 4,        // 动态 style
  PROPS = 8,        // 动态属性(非 class/style)
  FULL_PROPS = 16,  // 有动态 key 的属性
  HYDRATE_EVENTS = 32,
  STABLE_FRAGMENT = 64,
  KEYED_FRAGMENT = 128,
  UNKEYED_FRAGMENT = 256,
  NEED_PATCH = 512,
  DYNAMIC_SLOTS = 1024,
  HOISTED = -1,     // 静态提升
  BAIL = -2         // 退出优化模式
}

HoistStatic(静态提升)

将静态节点提升到渲染函数外部,避免重复创建。

<template>
  <div>
    <p>静态内容1</p>
    <p>静态内容2</p>
    <p>{{ dynamic }}</p>
  </div>
</template>

编译后:

// 静态节点被提升到模块顶层,只创建一次
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "静态内容1", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "静态内容2", -1 /* HOISTED */)

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,  // 复用静态节点
    _hoisted_2,  // 复用静态节点
    _createElementVNode("p", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
  ]))
}

编译优化对比

// Vue2 diff - 全量对比
// 每次更新都要遍历整个 vnode 树

// Vue3 diff - 靶向更新
// 1. 静态节点提升,不参与 diff
// 2. PatchFlag 标记动态内容类型
// 3. 只对比带有 patchFlag 的节点
// 4. 根据 patchFlag 值决定对比哪些属性

关键点

  • Fragment:支持多根节点,减少无意义的 DOM 层级
  • Teleport:将内容渲染到指定 DOM 位置,解决 CSS 层级问题
  • Suspense:统一处理异步组件加载状态
  • Tree-shaking:模块化设计,按需引入,减小打包体积
  • PatchFlag:编译时标记动态内容类型,运行时精准更新
  • HoistStatic:静态节点提升到渲染函数外,避免重复创建