Vue 3 核心 API 与实战要点速览

最近重温了一下 Vue 3, 把几个最容易踩坑、最影响代码质量的关键 API 整理了一下, 方便以后快速回顾。

1. 组合式 API vs 选项式 API

Vue 3 主推 组合式 API (Composition API) , 核心是用 setup 函数 + 一组响应式 API 组织代码。

js 复制代码
import { ref, computed, watch, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    watch(count, (newVal, oldVal) => {
      console.log(`count: ${oldVal} -> ${newVal}`)
    })
    onMounted(() => console.log('mounted!'))
    return { count, double }
  }
}

优势:

  • 同一业务逻辑的代码可以放在一起 (而不是分散在 data / methods / computed 里)
  • 逻辑复用更容易 (用 composable 函数)
  • TypeScript 推断更好

2. ref vs reactive ------ 选哪个?

API 适用场景 注意点
ref(0) 基本类型 + 对象/数组都通用 访问时要 .value (模板里不用)
reactive({}) 仅对象/数组 不能整体替换 (state = newObj 会失去响应性)

实际建议:

  • 优先用 ref ------ 行为可预测, 一致
  • 只有明确知道是对象 且需要直接访问属性 (如 state.user.name) 时用 reactive
js 复制代码
// 反例: reactive 整体替换会丢响应性
const state = reactive({ count: 0 })
state = { count: 1 }  // ❌ 失败, 模板不更新
// 正解 1: 用 ref
const state = ref({ count: 0 })
state.value = { count: 1 }  // ✅
// 正解 2: 用 Object.assign
Object.assign(state, { count: 1 })  // ✅

3. watch vs watchEffect ------ 怎么选?

  • watch(source, callback) ------ 显式指定监听源, 旧值新值都有
  • watchEffect(callback) ------ 自动收集依赖, 立刻执行一次, 简单但拿不到旧值
js 复制代码
// 场景 1: 监听单个 ref, 需要旧值
watch(count, (newVal, oldVal) => {
  console.log(`${oldVal} -> ${newVal}`)
})

// 场景 2: 副作用, 不需要旧值
watchEffect(() => {
  console.log(`count is now: ${count.value}`)
})

// 场景 3: 监听 reactive 对象的某个属性
watch(() => state.user.name, (newName) => {
  console.log(`name changed to: ${newName}`)
})

实战经验:

  • 90% 场景 watch 就够用, 显式更可控
  • 只有初始化时就要执行 的副作用用 watchEffect
  • 异步场景记得加 flush: 'post' 让 watcher 在 DOM 更新后跑

4. computed 的两个常见坑

坑 1: 试图传参数

js 复制代码
// ❌ computed 不支持参数
const filtered = computed((id) => items.value.filter(i => i.id === id))

// ✅ 用方法或 derived ref
function getById(id) {
  return items.value.find(i => i.id === id)
}

坑 2: 内部有副作用

js 复制代码
// ❌ computed 应该是纯函数, 副作用会重复执行
const data = computed(() => {
  fetch('/api/data')  // 不要这样做!
  return items.value
})

// ✅ 副作用放 watch / onMounted / 事件处理里

5. defineProps / defineEmits ------ <script setup> 写法

Vue 3.2+ 推荐的写法, 比 defineComponent 简洁很多:

vue 复制代码
<script setup>
import { ref } from 'vue'

const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 },
})

const emit = defineEmits(['change', 'submit'])

const localCount = ref(props.count)
</script>

<template>
  <h1>{{ title }}</h1>
  <button @click="emit('change', localCount)">+</button>
</template>

优势:

  • 不需要 return 暴露变量 (顶层声明就可用)
  • 自动按需引入 (unplugin-auto-import 配合更香)
  • TypeScript 支持更好

6. 响应式 API 的"破局"陷阱

数组 / 对象修改要保留引用

js 复制代码
const list = ref([1, 2, 3])

// ❌ 不会触发响应
list.value[0] = 99

// ✅ 用 splice
list.value.splice(0, 1, 99)

// ✅ 或用 splice 替代
list.value = [99, 2, 3]

解构 reactive 也会丢响应

js 复制代码
const state = reactive({ user: { name: 'tom' } })
const { user } = state  // ❌ user.name 修改不会触发响应
// ✅
const user = toRef(state, 'user')
// 或
const { user } = toRefs(state)

7. v-model 在自定义组件上的实现

父组件:

vue 复制代码
<CustomInput v-model="search" />

子组件 (<script setup>):

vue 复制代码
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

进阶 : 用 defineModel 宏 (Vue 3.4+) 可以省一半代码:

vue 复制代码
<script setup>
const model = defineModel()  // 自动双向绑定
</script>

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

8. 性能优化三件套

技巧 作用 适用
v-memo 缓存模板, 只在依赖变化时重渲染 大列表 / 复杂组件
<KeepAlive> 缓存组件实例 路由切换保留状态
shallowRef / shallowReactive 浅响应, 大对象性能好 不需要深层响应
vue 复制代码
<template>
  <!-- 只在 value 变化时重渲染 -->
  <HeavyList v-memo="[value]" :items="items" />
</template>

9. 总结

Vue 3 相比 Vue 2 主要升级:

  1. 响应式系统重写 (Proxy)
  2. Composition API ------ 逻辑复用更灵活
  3. <script setup> ------ 模板代码量减半
  4. Teleport / Suspense / Fragment ------ 解决 Vue 2 的痛点
  5. 更好的 TypeScript 支持

学习建议:

  • 不要死记 API, 重点理解响应式原理
  • 多写 composable 函数 (类似 React 的 custom hook)
  • 大型项目优先用 Pinia 替代 Vuex