Skip to content

Vue 3 新特性详解

一、Vue 3 概述

Vue 3 是 Vue.js 的最新主版本,于 2020 年 9 月发布。相比 Vue 2,Vue 3 在性能、可维护性、开发体验等方面都有显著提升。

核心改进

改进领域具体内容
性能提升虚拟 DOM 重写、diff算法优化、编译器优化、响应式系统改进
组合式 API更灵活的代码组织方式,更好的逻辑复用
TypeScript 支持源码使用 TypeScript 重写,类型推断更完善
Tree-shaking按需引入,减小打包体积
新内置组件Teleport、Suspense、Fragment
自定义渲染器轻松创建自定义渲染器

二、Composition API(组合式 API)

Composition API 是 Vue 3 最重要的新特性,提供了一种全新的组织组件逻辑的方式。

1. 与 Options API 对比

Options API(Vue 2 风格)

vue
<template>
  <div>
    <p>计数器: {{ count }}</p>
    <p>双倍: {{ doubled }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  computed: {
    doubled() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('组件已挂载')
  }
}
</script>

Composition API(Vue 3 风格)

vue
<template>
  <div>
    <p>计数器: {{ count }}</p>
    <p>双倍: {{ doubled }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}

onMounted(() => {
  console.log('组件已挂载')
})
</script>

2. 响应式 API

ref - 基本类型响应式

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

// 基本类型
const count = ref(0)
const message = ref('Hello')
const isActive = ref(true)

// 对象类型(内部会自动调用 reactive)
const user = ref({ name: '张三', age: 25 })

// 访问和修改
console.log(count.value) // 0
count.value++ // 修改
console.log(user.value.name) // 张三
</script>

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ message }}</p>
    <p>{{ user.name }}</p>
  </div>
</template>

使用场景

  • 管理基本类型数据(数字、字符串、布尔值)
  • 需要替换整个对象的场景
  • 需要明确区分响应式值和普通值的场景

reactive - 对象类型响应式

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

const state = reactive({
  count: 0,
  user: {
    name: '张三',
    profile: {
      email: 'zhangsan@example.com'
    }
  },
  items: ['a', 'b', 'c']
})

// 直接访问,无需 .value
console.log(state.count)
state.count++

// 嵌套对象也是响应式的
state.user.profile.email = 'new@example.com'

// 数组操作也是响应式的
state.items.push('d')
</script>

<template>
  <div>
    <p>{{ state.count }}</p>
    <p>{{ state.user.name }}</p>
    <ul>
      <li v-for="item in state.items" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>

使用场景

  • 管理复杂对象状态
  • 需要深度响应式的场景
  • 组件内部状态管理

ref vs reactive 对比

vue
<script setup>
import { ref, reactive, isRef, isReactive, toRef, toRefs } from 'vue'

// ref 特点
const countRef = ref(0)
countRef.value = 1 // 需要 .value

// reactive 特点
const stateReactive = reactive({ count: 0 })
stateReactive.count = 1 // 不需要 .value

// 解构响应式对象会失去响应性
const { count } = stateReactive // count 不再是响应式的!

// 使用 toRefs 保持响应性
const { count: reactiveCount } = toRefs(stateReactive)
reactiveCount.value = 2 // 保持响应性

// 使用 toRef 创建单个属性的 ref
const countRef2 = toRef(stateReactive, 'count')
</script>
特性refreactive
适用类型任意类型对象类型
访问方式需要 .value直接访问
解构保持响应性失去响应性(需用 toRefs)
重新赋值可以整体替换不能整体替换
TypeScript类型推断更简单需要类型标注

3. computed - 计算属性

vue
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 只读计算属性
const fullName = computed(() => `${firstName.value}${lastName.value}`)

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value}${lastName.value}`
  },
  set(newValue) {
    firstName.value = newValue.charAt(0)
    lastName.value = newValue.slice(1)
  }
})

// 复杂计算
const items = ref([
  { id: 1, name: '商品A', price: 100, quantity: 2 },
  { id: 2, name: '商品B', price: 200, quantity: 1 }
])

const totalPrice = computed(() => {
  return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})

const expensiveItems = computed(() => {
  return items.value.filter(item => item.price > 150)
})
</script>

<template>
  <div>
    <p>姓名: {{ fullName }}</p>
    <p>总价: {{ totalPrice }}</p>
    <p>高价商品: {{ expensiveItems.length }} 件</p>
  </div>
</template>

使用场景

  • 派生状态计算
  • 数据过滤和排序
  • 格式化显示数据
  • 缓存昂贵计算结果

4. watch 与 watchEffect

watch - 明确指定监听源

vue
<script setup>
import { ref, watch, reactive } from 'vue'

const count = ref(0)
const user = reactive({ name: '张三', age: 25 })

// 监听 ref
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})

// 监听 reactive 属性
watch(
  () => user.age,
  (newAge, oldAge) => {
    console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
  }
)

// 监听多个源
const firstName = ref('张')
const lastName = ref('三')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 ${oldFirst}${oldLast} 变为 ${newFirst}${newLast}`)
})

// 深度监听
const deepObject = ref({ nested: { value: 1 } })

watch(
  deepObject,
  newValue => {
    console.log('对象发生变化:', newValue)
  },
  { deep: true }
)

// 立即执行
watch(
  count,
  newValue => {
    console.log('当前值:', newValue)
  },
  { immediate: true }
)

// 监听对象内部变化
watch(
  () => ({ ...user }),
  newUser => {
    console.log('用户信息变化:', newUser)
  }
)
</script>

watchEffect - 自动追踪依赖

vue
<script setup>
import { ref, watchEffect, onScopeDispose } from 'vue'

const count = ref(0)
const multiplier = ref(2)

// 自动追踪所有使用的响应式依赖
const stop = watchEffect(onCleanup => {
  console.log(`结果: ${count.value * multiplier.value}`)

  // 清理函数(下次执行前调用)
  onCleanup(() => {
    console.log('清理副作用')
  })
})

// 停止监听
// stop()

// 带副作用的异步操作
const data = ref(null)
const loading = ref(false)

watchEffect(async onCleanup => {
  loading.value = true

  const controller = new AbortController()
  onCleanup(() => controller.abort())

  try {
    const response = await fetch('/api/data', {
      signal: controller.signal
    })
    data.value = await response.json()
  } catch (e) {
    if (e.name !== 'AbortError') {
      console.error(e)
    }
  } finally {
    loading.value = false
  }
})
</script>

watch vs watchEffect 对比

特性watchwatchEffect
依赖追踪手动指定自动追踪
获取旧值可以不可以
懒执行默认懒执行立即执行
使用场景需要精确控制监听源自动收集依赖

5. 生命周期钩子

vue
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured
} from 'vue'

onBeforeMount(() => {
  console.log('组件挂载前')
})

onMounted(() => {
  console.log('组件已挂载,DOM 可访问')
  // 初始化第三方库、DOM 操作、发起请求
})

onBeforeUpdate(() => {
  console.log('组件更新前')
})

onUpdated(() => {
  console.log('组件已更新')
  // 注意:避免在此修改响应式状态,可能导致无限循环
})

onBeforeUnmount(() => {
  console.log('组件卸载前')
  // 清理定时器、取消订阅等
})

onUnmounted(() => {
  console.log('组件已卸载')
})

// KeepAlive 相关
onActivated(() => {
  console.log('组件被激活')
})

onDeactivated(() => {
  console.log('组件被停用')
})

// 错误捕获
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err)
  return false // 阻止错误继续传播
})
</script>

生命周期映射表

Options APIComposition API
beforeCreatesetup()
createdsetup()
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
activatedonActivated
deactivatedonDeactivated
errorCapturedonErrorCaptured

6. 依赖注入 (provide/inject)

vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref, readonly } from 'vue'

const theme = ref('dark')
const user = ref({ name: '张三' })

// 提供只读数据
provide('theme', readonly(theme))

// 提供可修改数据(通过方法)
provide('updateTheme', newTheme => {
  theme.value = newTheme
})

// 提供响应式对象
provide('user', user)
</script>

<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'

// 注入数据
const theme = inject('theme', 'light') // 第二个参数是默认值
const updateTheme = inject('updateTheme')
const user = inject('user')

// 使用 Symbol 作为 key(推荐)
const ThemeKey = Symbol('theme')
provide(ThemeKey, theme)
</script>

最佳实践 - 使用 Symbol 和类型

typescript
// keys.ts
import type { InjectionKey, Ref } from 'vue'

export interface Theme {
  mode: 'light' | 'dark'
  toggle: () => void
}

export const ThemeKey: InjectionKey<Theme> = Symbol('theme')

// 祖先组件
import { provide, ref } from 'vue'
import { ThemeKey } from './keys'

const mode = ref<'light' | 'dark'>('light')
provide(ThemeKey, {
  mode: mode.value,
  toggle: () => {
    mode.value = mode.value === 'light' ? 'dark' : 'light'
  }
})

// 后代组件
import { inject } from 'vue'
import { ThemeKey } from './keys'

const theme = inject(ThemeKey)!

使用场景

  • 深层嵌套组件通信
  • 主题配置传递
  • 国际化设置
  • 用户认证状态

7. 组合式函数 (Composables)

组合式函数是 Vue 3 中复用逻辑的核心方式。

基础示例

typescript
// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const doubled = computed(() => count.value * 2)

  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    doubled,
    increment,
    decrement,
    reset
  }
}

// 使用
<script setup>
import { useCounter } from './composables/useCounter'

const { count, doubled, increment, reset } = useCounter(10)
</script>

实用组合式函数示例

typescript
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(event: MouseEvent) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// composables/useFetch.ts
import { ref, watchEffect, toValue } from 'vue'
import type { Ref } from 'vue'

export function useFetch<T>(url: Ref<string> | string) {
  const data = ref<T | null>(null) as Ref<T | null>
  const error = ref<Error | null>(null)
  const loading = ref(false)

  async function fetchData() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url))
      if (!response.ok) throw new Error(response.statusText)
      data.value = await response.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error, loading, refetch: fetchData }
}

// composables/useLocalStorage.ts
import { ref, watch } from 'vue'

export function useLocalStorage<T>(key: string, defaultValue: T) {
  const stored = localStorage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue)

  watch(
    data,
    newValue => {
      localStorage.setItem(key, JSON.stringify(newValue))
    },
    { deep: true }
  )

  return data
}

// composables/useDebounce.ts
import { ref, watch } from 'vue'

export function useDebounce<T>(value: Ref<T>, delay = 300) {
  const debouncedValue = ref(value.value) as Ref<T>
  let timeout: ReturnType<typeof setTimeout>

  watch(value, newValue => {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      debouncedValue.value = newValue
    }, delay)
  })

  return debouncedValue
}

// composables/useToggle.ts
import { ref } from 'vue'

export function useToggle(initialValue = false) {
  const value = ref(initialValue)

  function toggle() {
    value.value = !value.value
  }

  function setTrue() {
    value.value = true
  }

  function setFalse() {
    value.value = false
  }

  return { value, toggle, setTrue, setFalse }
}

使用示例

vue
<script setup>
import { useMouse } from './composables/useMouse'
import { useFetch } from './composables/useFetch'
import { useLocalStorage } from './composables/useLocalStorage'
import { useToggle } from './composables/useToggle'

const { x, y } = useMouse()
const { data, loading, error } = useFetch('/api/users')
const theme = useLocalStorage('theme', 'light')
const { value: isOpen, toggle } = useToggle()
</script>

<template>
  <div>
    <p>鼠标位置: {{ x }}, {{ y }}</p>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <div v-else>{{ data }}</div>
    <button @click="toggle">{{ isOpen ? '关闭' : '打开' }}</button>
  </div>
</template>

组合式函数最佳实践

  1. 命名以 use 开头
  2. 返回值使用响应式引用
  3. setup<script setup> 中调用
  4. 返回的函数保持响应式引用的连接

三、响应式系统重构(Proxy)

Vue 3 使用 Proxy 重写了响应式系统,解决了 Vue 2 的诸多限制。

1. Vue 2 响应式限制

javascript
// Vue 2 的限制

// 1. 无法检测对象属性的添加/删除
this.obj.newProp = 'value' // 不是响应式的
Vue.set(this.obj, 'newProp', 'value') // 需要使用 Vue.set

// 2. 无法检测数组索引赋值
this.arr[0] = 'new value' // 不是响应式的
Vue.set(this.arr, 0, 'new value') // 需要使用 Vue.set

// 3. 无法检测数组长度修改
this.arr.length = 0 // 不是响应式的

// 4. 对象新增属性需要 Vue.set

2. Vue 3 Proxy 响应式

vue
<script setup>
import { reactive, ref } from 'vue'

// 对象
const obj = reactive({ name: '张三' })
obj.age = 25 // 自动响应式!
delete obj.name // 自动响应式!

// 数组
const arr = ref([1, 2, 3])
arr.value[0] = 100 // 自动响应式!
arr.value.length = 0 // 自动响应式!

// Map 和 Set
const map = reactive(new Map())
map.set('key', 'value') // 自动响应式!

const set = reactive(new Set())
set.add('item') // 自动响应式!
</script>

3. 响应式工具函数

vue
<script setup>
import {
  reactive,
  ref,
  readonly,
  shallowRef,
  shallowReactive,
  toRaw,
  markRaw,
  isReactive,
  isRef,
  toRef,
  toRefs
} from 'vue'

// readonly - 只读响应式
const original = reactive({ count: 0 })
const copy = readonly(original)
// copy.count++ // 警告:目标是只读的

// shallowRef - 浅层响应式 ref
const shallow = shallowRef({ nested: { value: 1 } })
shallow.value = { nested: { value: 2 } } // 触发更新
shallow.value.nested.value = 3 // 不触发更新

// shallowReactive - 浅层响应式 reactive
const shallowObj = shallowReactive({
  count: 1,
  nested: { value: 2 }
})
shallowObj.count++ // 触发更新
shallowObj.nested.value = 3 // 不触发更新

// toRaw - 获取原始对象
const raw = toRaw(original)

// markRaw - 标记对象永不转为响应式
const obj = markRaw({})
const reactiveObj = reactive(obj) // obj 不会被转为响应式

// 类型检查
console.log(isReactive(original)) // true
console.log(isRef(shallow)) // true
</script>

使用场景对比

API使用场景
reactive复杂对象状态管理
ref基本类型、需要替换整个对象
shallowRef大型不可变数据结构
shallowReactive只需要顶层响应式
readonly防止数据被修改
markRaw第三方库实例、不需要响应式的对象

四、Teleport 组件

Teleport 允许将组件渲染到 DOM 树的其他位置。

1. 基本用法

vue
<template>
  <div class="container">
    <h1>主容器</h1>

    <!-- 将模态框渲染到 body 下 -->
    <Teleport to="body">
      <div class="modal" v-if="showModal">
        <div class="modal-content">
          <h2>模态框标题</h2>
          <p>模态框内容</p>
          <button @click="showModal = false">关闭</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

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

<style>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

2. 实际应用示例

vue
<!-- Modal.vue -->
<template>
  <Teleport to="body">
    <Transition name="modal">
      <div v-if="modelValue" class="modal-overlay" @click.self="close">
        <div class="modal-container">
          <header class="modal-header">
            <slot name="header">
              <h3>{{ title }}</h3>
            </slot>
            <button class="close-btn" @click="close">&times;</button>
          </header>

          <main class="modal-body">
            <slot></slot>
          </main>

          <footer class="modal-footer">
            <slot name="footer">
              <button @click="close">取消</button>
              <button class="primary" @click="confirm">确认</button>
            </slot>
          </footer>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

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

const props = defineProps({
  modelValue: Boolean,
  title: String
})

const emit = defineEmits(['update:modelValue', 'confirm'])

function close() {
  emit('update:modelValue', false)
}

function confirm() {
  emit('confirm')
  close()
}

// 防止背景滚动
watch(
  () => props.modelValue,
  val => {
    document.body.style.overflow = val ? 'hidden' : ''
  }
)
</script>

<!-- 使用 -->
<template>
  <button @click="showModal = true">打开模态框</button>

  <Modal v-model="showModal" title="确认操作" @confirm="handleConfirm">
    <p>确定要执行此操作吗?</p>
  </Modal>
</template>

3. 多个 Teleport

vue
<template>
  <!-- 多个 Teleport 会按顺序追加到目标位置 -->
  <Teleport to="#notifications">
    <div class="notification">通知 1</div>
  </Teleport>

  <Teleport to="#notifications">
    <div class="notification">通知 2</div>
  </Teleport>

  <!-- 动态目标 -->
  <Teleport :to="targetSelector">
    <div>动态传送</div>
  </Teleport>

  <!-- 禁用 Teleport -->
  <Teleport to="body" :disabled="isDisabled">
    <div>条件传送</div>
  </Teleport>
</template>

<script setup>
import { ref } from 'vue'
const targetSelector = ref('#app')
const isDisabled = ref(false)
</script>

使用场景

  • 模态框、对话框
  • 通知提示
  • 全局加载动画
  • 下拉菜单(避免被父容器 overflow 裁剪)

五、Fragments(多根节点)

Vue 3 组件支持多个根节点。

1. Vue 2 的限制

vue
<!-- Vue 2 - 必须有单一根节点 -->
<template>
  <div>
    <!-- 必须包裹 -->
    <h1>标题</h1>
    <p>内容</p>
  </div>
</template>

2. Vue 3 多根节点

vue
<!-- Vue 3 - 支持多根节点 -->
<template>
  <h1>标题</h1>
  <p>内容</p>
  <footer>页脚</footer>
</template>

<!-- 实际应用:列表项组件 -->
<template>
  <td>{{ item.name }}</td>
  <td>{{ item.price }}</td>
  <td>{{ item.quantity }}</td>
</template>

<!-- 实际应用:文本组件 -->
<template>
  <span class="prefix">{{ prefix }}</span>
  <span class="content">{{ content }}</span>
  <span class="suffix">{{ suffix }}</span>
</template>

3. Attributes 继承

vue
<script setup>
defineOptions({
  inheritAttrs: false // 禁用自动继承
})
</script>

<template>
  <!-- 多根节点时,需要手动绑定 $attrs -->
  <header v-bind="$attrs">
    <h1>标题</h1>
  </header>
  <main>
    <slot></slot>
  </main>
</template>

使用场景

  • 表格单元格组件
  • 文本格式化组件
  • 减少不必要的 DOM 嵌套

六、Suspense 组件

Suspense 用于协调异步依赖的加载状态。

1. 基本用法

vue
<template>
  <Suspense>
    <!-- 默认插槽:异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>

    <!-- fallback 插槽:加载状态 -->
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

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

const AsyncComponent = defineAsyncComponent(
  () => import('./components/HeavyComponent.vue')
)
</script>

2. 异步 setup

vue
<!-- AsyncUser.vue -->
<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

<script setup>
const props = defineProps(['id'])

// async setup 会让组件变成异步依赖
const user = await fetch(`/api/users/${props.id}`).then(r => r.json())
</script>

<!-- 父组件 -->
<template>
  <Suspense>
    <AsyncUser :id="userId" />

    <template #fallback>
      <div class="skeleton">
        <div class="skeleton-avatar"></div>
        <div class="skeleton-text"></div>
      </div>
    </template>
  </Suspense>
</template>

3. 错误处理

vue
<template>
  <Suspense @pending="onPending" @resolve="onResolve" @fallback="onFallback">
    <template #default>
      <AsyncComponent />
    </template>

    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured(e => {
  error.value = e
  return false // 阻止错误继续传播
})

function onPending() {
  console.log('开始加载')
}

function onResolve() {
  console.log('加载完成')
}

function onFallback() {
  console.log('显示 fallback')
}
</script>

4. 嵌套 Suspense

vue
<template>
  <Suspense>
    <template #default>
      <Header />
      <main>
        <Suspense suspensible>
          <!-- suspensible: 等待父级 -->
          <AsyncContent />
          <template #fallback>
            <ContentSkeleton />
          </template>
        </Suspense>
      </main>
      <Footer />
    </template>

    <template #fallback>
      <FullPageLoader />
    </template>
  </Suspense>
</template>

使用场景

  • 异步组件加载
  • 数据预取
  • 代码分割
  • 服务端渲染 (SSR)

七、更好的 TypeScript 支持

Vue 3 源码使用 TypeScript 重写,提供了完整的类型支持。

1. 类型定义

typescript
<script setup lang="ts">
import { ref, reactive, computed, type Ref, type PropType } from 'vue'

// ref 类型
const count = ref<number>(0)
const message = ref<string>('hello')

// reactive 类型
interface User {
  id: number
  name: string
  email: string
}
const user = reactive<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
})

// computed 类型
const doubleCount = computed<number>(() => count.value * 2)

// Props 类型
interface Props {
  title: string
  count?: number
  items: string[]
  user: {
    name: string
    age: number
  }
}

const props = defineProps<Props>()

// 带默认值的 Props
const props = withDefaults(defineProps<Props>(), {
  title: '默认标题',
  count: 0
})

// Emits 类型
interface Emits {
  (e: 'update', value: number): void
  (e: 'delete', id: number): void
  (e: 'change', name: string, age: number): void
}

const emit = defineEmits<Emits>()

// 复杂 Props 类型
const props = defineProps({
  items: {
    type: Array as PropType<string[]>,
    required: true
  },
  config: {
    type: Object as PropType<{ theme: string; size: number }>,
    default: () => ({ theme: 'light', size: 14 })
  }
})
</script>

2. 泛型组件

vue
<!-- GenericList.vue -->
<script setup lang="ts" generic="T">
import { type PropType } from 'vue'

interface Props {
  items: T[]
  renderItem: (item: T) => string
}

defineProps<Props>()
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ renderItem(item) }}
    </li>
  </ul>
</template>

<!-- 使用 -->
<script setup lang="ts">
import GenericList from './GenericList.vue'

interface User {
  id: number
  name: string
}

const users: User[] = [
  { id: 1, name: '张三' },
  { id: 2, name: '李四' }
]
</script>

<template>
  <GenericList :items="users" :render-item="user => user.name" />
</template>

3. 类型工具

typescript
import type {
  Ref,
  ComputedRef,
  ReactiveEffect,
  ComponentPublicInstance,
  VNode,
  Component,
  PropType,
  ExtractPropTypes
} from 'vue'

// 提取 Props 类型
const propsDefinition = {
  title: String,
  count: { type: Number, default: 0 }
} as const

type Props = ExtractPropTypes<typeof propsDefinition>
// { title: string; count: number }

// 组件实例类型
type MyComponentInstance = ComponentPublicInstance<
  {},
  {},
  { localState: string },
  { method(): void }
>

// VNode 类型
const vnode: VNode = h('div', 'hello')

八、v-model 改进

Vue 3 对 v-model 进行了重大改进。

1. Vue 2 vs Vue 3

vue
<!-- Vue 2 -->
<MyComponent v-model="value" />

<!-- 等价于 -->
<MyComponent :value="value" @input="value = $event" />

<!-- Vue 3 -->
<MyComponent v-model="value" />

<!-- 等价于 -->
<MyComponent :modelValue="value" @update:modelValue="value = $event" />

2. 多个 v-model

vue
<!-- 父组件 -->
<template>
  <UserForm
    v-model:firstName="firstName"
    v-model:lastName="lastName"
    v-model:email="email"
  />
</template>

<!-- UserForm.vue -->
<script setup>
const firstName = defineModel < string > 'firstName'
const lastName = defineModel < string > 'lastName'
const email = defineModel < string > 'email'
</script>

<template>
  <input v-model="firstName" placeholder="名" />
  <input v-model="lastName" placeholder="姓" />
  <input v-model="email" placeholder="邮箱" />
</template>

3. v-model 修饰符

vue
<!-- 使用内置修饰符 -->
<MyInput v-model.trim="text" />
<MyInput v-model.number="count" />
<MyInput v-model.lazy="value" />

<!-- 自定义修饰符 -->
<script setup>
const [modelValue, modelModifiers] = defineModel({
  set(value) {
    if (modelModifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input :value="modelValue" @input="modelValue = $event.target.value" />
</template>

<!-- 使用 -->
<MyInput v-model.capitalize="name" />

4. 自定义 v-model 参数

vue
<script setup>
// 定义带参数的 v-model
const title =
  defineModel <
  string >
  ('title',
  {
    default: '默认标题',
    required: true,
    type: String,
    validator: value => value.length > 0
  })
</script>

<template>
  <input v-model="title" />
</template>

<!-- 使用 -->
<CustomInput v-model:title="pageTitle" />

九、指令 API 变化

1. 指令钩子变化

typescript
// Vue 2
const directive = {
  bind(el, binding, vnode) {
    // 指令首次绑定到元素时
  },
  inserted(el, binding, vnode) {
    // 被绑定元素插入父节点时
  },
  update(el, binding, vnode, oldVnode) {
    // VNode 更新时
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // VNode 及其子 VNode 全部更新后
  },
  unbind(el, binding, vnode) {
    // 指令与元素解绑时
  }
}

// Vue 3
const directive = {
  created(el, binding, vnode, prevVnode) {
    // 元素被插入 DOM 前
  },
  beforeMount(el, binding, vnode, prevVnode) {
    // 元素被插入 DOM 前(类似 bind)
  },
  mounted(el, binding, vnode, prevVnode) {
    // 元素被插入 DOM 后(类似 inserted)
  },
  beforeUpdate(el, binding, vnode, prevVnode) {
    // VNode 更新前
  },
  updated(el, binding, vnode, prevVnode) {
    // VNode 更新后(类似 componentUpdated)
  },
  beforeUnmount(el, binding, vnode, prevVnode) {
    // 卸载前
  },
  unmounted(el, binding, vnode, prevVnode) {
    // 卸载后(类似 unbind)
  }
}

2. 自定义指令示例

typescript
// directives/vFocus.ts
import type { Directive, DirectiveBinding } from 'vue'

export const vFocus: Directive<HTMLElement> = {
  mounted(el) {
    el.focus()
  }
}

// directives/vClickOutside.ts
import type { Directive } from 'vue'

export const vClickOutside: Directive<HTMLElement, (e: MouseEvent) => void> = {
  mounted(el, binding) {
    el._clickOutside = (event: MouseEvent) => {
      if (!(el === event.target || el.contains(event.target as Node))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside)
  }
}

// directives/vPermission.ts
import type { Directive } from 'vue'
import { useUserStore } from '@/stores/user'

export const vPermission: Directive<HTMLElement, string[]> = {
  mounted(el, binding) {
    const userStore = useUserStore()
    const { value: permissions } = binding

    const hasPermission = permissions.some(permission =>
      userStore.permissions.includes(permission)
    )

    if (!hasPermission) {
      el.parentNode?.removeChild(el)
    }
  }
}

// directives/vLazy.ts - 图片懒加载
import type { Directive } from 'vue'

export const vLazy: Directive<HTMLImageElement, string> = {
  mounted(el, binding) {
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    })

    observer.observe(el)
    el._observer = observer
  },
  unmounted(el) {
    el._observer?.disconnect()
  }
}

3. 全局注册指令

typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { vFocus, vClickOutside, vPermission, vLazy } from './directives'

const app = createApp(App)

app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.directive('permission', vPermission)
app.directive('lazy', vLazy)

app.mount('#app')

十、全局 API 重构

Vue 3 重构了全局 API,支持 Tree-shaking。

1. createApp

typescript
// Vue 2
import Vue from 'vue'
import App from './App.vue'
import Router from './router'
import Store from './store'

Vue.use(Router)
Vue.use(Store)
Vue.prototype.$http = http

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

// Vue 3
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)

app.use(router)
app.use(createPinia())
app.provide('http', http) // 全局注入

app.mount('#app')

2. 全局配置

typescript
// Vue 2
Vue.config.ignoredElements = ['my-custom-element']
Vue.config.productionTip = false

// Vue 3
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('my-')
app.config.errorHandler = (err, instance, info) => {
  console.error(err, info)
}
app.config.warnHandler = (msg, instance, trace) => {
  console.warn(msg, trace)
}
app.config.performance = true

3. 应用级 API

typescript
const app = createApp(App)

// 全局组件
app.component('MyButton', {
  template: '<button><slot /></button>'
})

// 全局指令
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// 全局 mixin(不推荐)
app.mixin({
  created() {
    console.log('全局 mixin')
  }
})

// 全局 provide
app.provide('globalConfig', {
  theme: 'dark',
  locale: 'zh-CN'
})

// 多个应用实例
const app1 = createApp(App1).mount('#app1')
const app2 = createApp(App2).mount('#app2')

十一、性能优化

1. 编译器优化

vue
<!-- 静态提升 -->
<template>
  <div>
    <!-- 静态内容会被提升到 render 函数外 -->
    <div class="static">
      <span>静态文本</span>
    </div>

    <!-- 动态内容 -->
    <div>{{ dynamicContent }}</div>
  </div>
</template>

<!-- 编译结果(简化) -->
<script>
const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
  'div',
  { class: 'static' },
  [/*#__PURE__*/ _createElementVNode('span', null, '静态文本')],
  -1
)

function render() {
  return _createElementBlock('div', null, [
    _hoisted_1, // 静态节点复用
    _createElementVNode('div', null, _toDisplayString(dynamicContent))
  ])
}
</script>

2. Patch Flags

vue
<template>
  <!-- 不同的 patch flags -->
  <div class="static">静态 class</div>
  <div :class="dynamicClass">动态 class</div>
  <div :style="dynamicStyle">动态 style</div>
  <div>{{ text }}>文本内容</div>
  <div :id="dynamicId" :class="dynamicClass">多个动态属性</div>
</template>

<!-- 编译后的 patch flags -->
<!-- 
  1 = TEXT - 文本内容动态
  2 = CLASS - class 动态
  4 = STYLE - style 动态
  8 = PROPS - 属性动态
  16 = FULL_PROPS - 完整属性动态
  32 = HYDRATE_EVENTS - 事件监听器
  64 = STABLE_FRAGMENT - 稳定的片段
  128 = KEYED_FRAGMENT - 带 key 的片段
  256 = UNKEYED_FRAGMENT - 不带 key 的片段
-->

3. Tree-shaking

typescript
// Vue 2 - 全量引入
import Vue from 'vue'
// 即使只用了 ref,也会打包整个 Vue

// Vue 3 - 按需引入
import { ref, computed, watch, onMounted } from 'vue'
// 只打包使用到的 API

// 未使用的 API 会被移除
import { ref } from 'vue'
// computed, watch 等未使用的不会被打包

4. 虚拟列表优化

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

const items = ref(
  Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    text: `Item ${i}`
  }))
)

const visibleStart = ref(0)
const visibleEnd = ref(20)
const itemHeight = 50

function onScroll(e) {
  const scrollTop = e.target.scrollTop
  visibleStart.value = Math.floor(scrollTop / itemHeight)
  visibleEnd.value = visibleStart.value + 20
}

const visibleItems = computed(() =>
  items.value.slice(visibleStart.value, visibleEnd.value)
)
</script>

<template>
  <div class="container" @scroll="onScroll">
    <div class="spacer" :style="{ height: `${items.length * itemHeight}px` }">
      <div
        v-for="item in visibleItems"
        :key="item.id"
        class="item"
        :style="{
          height: `${itemHeight}px`,
          transform: `translateY(${item.id * itemHeight}px)`
        }"
      >
        {{ item.text }}
      </div>
    </div>
  </div>
</template>

十二、其他重要特性

1. defineComponent

typescript
import { defineComponent, ref, PropType } from 'vue'

export default defineComponent({
  name: 'MyComponent',

  props: {
    title: {
      type: String,
      required: true
    },
    items: {
      type: Array as PropType<string[]>,
      default: () => []
    }
  },

  setup(props, { emit, slots, attrs, expose }) {
    const count = ref(0)

    function increment() {
      count.value++
      emit('update', count.value)
    }

    expose({ increment }) // 暴露给父组件

    return { count, increment }
  }
})

2. defineAsyncComponent

typescript
import { defineAsyncComponent } from 'vue'

// 简单用法
const AsyncComponent = defineAsyncComponent(
  () => import('./components/MyComponent.vue')
)

// 完整选项
const AsyncComponentWithOptions = defineAsyncComponent({
  loader: () => import('./components/MyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 10000,
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) {
      retry()
    } else {
      fail()
    }
  }
})

3. defineCustomElement

typescript
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  props: {
    message: String
  },
  template: `
    <style>
      :host { display: block; }
      .text { color: blue; }
    </style>
    <span class="text">{{ message }}</span>
  `
})

customElements.define('my-vue-element', MyVueElement)

// 使用
// <my-vue-element message="Hello"></my-vue-element>

4. h 函数(渲染函数)

typescript
import { h, defineComponent, ref } from 'vue'

export default defineComponent({
  setup() {
    const count = ref(0)

    return () => h('div', [
      h('h1', '计数器'),
      h('p', `当前值: ${count.value}`),
      h('button', {
        onClick: () => count.value++
      }, '增加')
    ])
  }
})

// JSX 写法(需要配置)
export default defineComponent({
  setup() {
    const count = ref(0)

    return () => (
      <div>
        <h1>计数器</h1>
        <p>当前值: {count.value}</p>
        <button onClick={() => count.value++}>增加</button>
      </div>
    )
  }
})

5. CSS 中的 v-bind

vue
<template>
  <div class="dynamic-color">动态颜色</div>
</template>

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

const themeColor = ref('#42b883')
const fontSize = ref(16)
</script>

<style>
.dynamic-color {
  color: v-bind(themeColor);
  font-size: v-bind(fontSize + 'px');
}
</style>

6. CSS Scoped 改进

vue
<template>
  <!-- 深度选择器 -->
  <div class="container">
    <ChildComponent />
  </div>
</template>

<style scoped>
/* Vue 2 深度选择器 */
.container ::v-deep .child-class {
}

/* Vue 3 深度选择器(推荐) */
.container :deep(.child-class) {
}

/* 插槽选择器 */
:slotted(.slot-class) {
}

/* 全局选择器 */
:global(.global-class) {
}
</style>

十三、迁移指南要点

1. 破坏性变更概览

变更项Vue 2Vue 3
全局 APInew Vue()createApp()
v-modelvalue + inputmodelValue + update:modelValue
v-if 与 v-forv-for 优先级更高v-if 优先级更高
生命周期destroyedunmounted
过滤器支持移除
$on/$off/$once支持移除
内联模板支持移除

2. 迁移构建版本

javascript
// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.resolve.alias.set('vue', '@vue/compat')
  }
}

3. 兼容性配置

javascript
import { createApp } from 'vue'

const app = createApp(App)

app.config.compilerOptions = {
  compatConfig: {
    MODE: 2, // 兼容模式
    GLOBAL_MOUNT: false
  }
}

十四、最佳实践总结

1. 响应式选择

基本类型 → ref
对象类型 → reactive
需要替换整个对象 → ref
需要解构 → reactive + toRefs
大型不可变数据 → shallowRef
第三方类实例 → markRaw + shallowRef

2. 组件设计原则

单一职责 → 拆分为小组件
逻辑复用 → 组合式函数
跨组件通信 → provide/inject 或状态管理
大型表单 → 组合式函数 + 分步组件

3. 性能优化策略

大型列表 → 虚拟滚动
异步组件 → defineAsyncComponent + Suspense
静态内容 → v-once
频繁更新 → shallowRef/shallowReactive
计算缓存 → computed

总结

Vue 3 带来了革命性的改进:

  1. Composition API 提供了更灵活的代码组织方式和更好的逻辑复用能力
  2. Proxy 响应式系统 解决了 Vue 2 的响应式限制
  3. 新内置组件(Teleport、Suspense、Fragment)扩展了组件能力
  4. TypeScript 支持 提供了完整的类型推断
  5. 性能优化 通过编译器优化和 Tree-shaking 减小了打包体积
  6. 更好的开发体验 更清晰的 API 设计和更好的错误提示

这些特性使得 Vue 3 成为构建现代 Web 应用的强大工具,无论是小型项目还是大型企业级应用都能从中受益。

基于 VitePress 的本地知识库