Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Vue 生态 / Vue Ecosystem

1. Vue 生态知识体系 / Vue Ecosystem Knowledge System

2. Vue 组件、模板与编译原理 / Vue Components, Templates, and Compilation

3. Vue 3 响应式系统与 Composition API / Vue 3 Reactivity System and Composition API

4. Vue Router、表单与组件架构 / Vue Router, Forms, and Component Architecture

5. Pinia 状态管理与持久化 / Pinia State Management and Persistence

6. Nuxt 路由、渲染与项目结构 / Nuxt Routing, Rendering, and Project Structure

7. Nuxt 数据获取、Server API 与缓存 / Nuxt Data Fetching, Server APIs, and Caching

8. Vue 测试、性能与生产工程 / Vue Testing, Performance, and Production Engineering

本页目录

Vue 3 响应式系统与 Composition API / Vue 3 Reactivity System and Composition API ​

📅 创建时间:2026-07-28 🏷️ 标签:#Vue #Reactivity #CompositionAPI #Composables 📚 前置知识:[[01-vue-components-and-templates]]


📋 本章目标 ​

  • 理解 Vue 3 Proxy-based 响应式系统的底层原理(track / trigger / effect)
  • 掌握 ref、reactive、shallowRef、shallowReactive 的适用场景与决策方法
  • 理解 computed 的惰性求值、缓存机制及与 watch 的本质区别
  • 掌握 watch、watchEffect、watchPostEffect、watchSyncEffect 的执行时序与清理机制
  • 能够设计类型安全、生命周期受控的可组合函数(Composables)
  • 了解 Vue 3.3 / 3.4 / 3.5 引入的关键新特性及其使用方式
  • 能够完成从 Vue 2 Options API 到 Vue 3 Composition API 的迁移

第1部分:Proxy 响应式原理图解 ​

1.1 从 Object.defineProperty 到 Proxy ​

Vue 2 使用 Object.defineProperty 劫持对象属性的 getter/setter,这带来三个先天缺陷:无法检测属性新增/删除、无法直接监听数组索引变更、必须在初始化时深度遍历所有嵌套属性。Vue 3 改用 Proxy,从根本上解决了这些问题。

┌─────────────────────────────────────────────────────────────┐
│              Vue 2 vs Vue 3 响应式实现对比                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Vue 2 (Object.defineProperty)                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ❌ 新增属性检测  → 需 Vue.set(obj, key, val)        │   │
│  │  ❌ 删除属性检测  → 需 Vue.delete(obj, key)          │   │
│  │  ❌ 数组索引赋值  → arr[3] = x 不触发更新            │   │
│  │  ❌ 初始化递归    → 大对象首次渲染慢                  │   │
│  │  ✅ IE 兼容      → 仅此一个优势                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Vue 3 (Proxy)                                              │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ✅ 新增属性检测  → proxy.newProp = val 自动追踪     │   │
│  │  ✅ 删除属性检测  → delete proxy.prop 自动触发       │   │
│  │  ✅ 数组索引/长度 → 原生数组操作全部响应              │   │
│  │  ✅ 惰性响应      → 访问时才递归,不访问不追踪       │   │
│  │  ✅ 支持 Map/Set/WeakMap/WeakSet                    │   │
│  │  ❌ 不支持 IE     → 无法 polyfill Proxy             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

1.2 核心三要素:targetMap / track / trigger ​

Vue 3 响应式系统的核心是一个发布-订阅模型,由三个关键部分构成:

  • targetMap(WeakMap):全局的依赖映射表,结构为 WeakMap<target, Map<key, Set<effect>>>
  • track(依赖收集):在 Proxy 的 get trap 中调用,将当前正在运行的 effect 注册到对应属性的依赖集合中
  • trigger(派发更新):在 Proxy 的 set / deleteProperty trap 中调用,查找所有依赖该属性的 effect 并重新执行
┌─────────────────────────────────────────────────────────────┐
│              响应式系统核心数据流                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   component.render() 执行                                   │
│         │                                                   │
│         ▼                                                   │
│   ┌──────────┐     读取 state.x      ┌──────────┐          │
│   │  effect   │ ──────────────────→  │  Proxy   │          │
│   │(渲染函数) │ ←── 注册依赖 ──────  │  get()   │          │
│   └──────────┘                      └──────────┘          │
│         │                                │                  │
│         │                          track(target, key)       │
│         │                                │                  │
│         │                                ▼                  │
│         │                      ┌──────────────────┐        │
│         │                      │   targetMap       │        │
│         │                      │                   │        │
│         │                      │ WeakMap {         │        │
│         │                      │   state → Map {   │        │
│         │                      │     "x" → Set {   │        │
│         │                      │       renderEffect │        │
│         │                      │     }              │        │
│         │                      │   }                │        │
│         │                      │ }                  │        │
│         │                      └──────────────────┘        │
│         │                                                   │
│         │    state.x = 10                                   │
│         │         │                                         │
│         │         ▼                                         │
│         │   ┌──────────┐     写入 state.x      ┌──────────┐ │
│         │   │  Proxy   │ ──────────────────→  │  新值    │ │
│         │   │  set()   │                      │   10     │ │
│         │   └──────────┘                      └──────────┘ │
│         │         │                                         │
│         │   trigger(target, key)                             │
│         │         │                                         │
│         │         ▼                                         │
│         │   查找 targetMap 中所有依赖 "x" 的 effect          │
│         │         │                                         │
│         │         ▼                                         │
│         │   ┌──────────┐                                    │
│         └──→│  effect   │  重新执行(组件重新渲染)          │
│             │(渲染函数) │                                    │
│             └──────────┘                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

1.3 Effect 作为"订阅者" ​

effect 是响应式系统中的一等公民 —— 任何需要响应式地重新执行的函数都被包裹为 effect。组件的渲染函数、computed 的 getter、watch 的回调、watchEffect 的回调,本质上都是 effect。

ts
// Vue 3 内部 effect 的简化实现概念
let activeEffect: ReactiveEffect | null = null

class ReactiveEffect {
  deps: Set<Dep>[] = []       // 记录该 effect 被哪些属性依赖集合引用
  private _fn: () => unknown   // 用户传入的函数

  constructor(fn: () => unknown) {
    this._fn = fn
  }

  run() {
    // 将当前 effect 设置为全局 activeEffect
    // track() 在 Proxy get 中读取 activeEffect 并注册依赖
    activeEffect = this
    const result = this._fn()
    activeEffect = null
    return result
  }
}

// track: 在 get 中调用,建立 属性 → effect 的映射
function track(target: object, key: string | symbol) {
  if (!activeEffect) return  // 没有 effect 在运行,无需收集
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

// trigger: 在 set/deleteProperty 中调用,通知所有依赖更新
function trigger(target: object, key: string | symbol) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect.run())
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

1.4 批量更新与 nextTick / Scheduler ​

连续多次修改响应式数据时,每次 set 都会触发 trigger,但 Vue 不会立即同步执行 effect —— 这样会导致同一个组件在一次同步操作中被渲染多次。Vue 使用 scheduler(调度器) 将 effect 的执行推迟到微任务队列,实现批量更新。

┌─────────────────────────────────────────────────────────────┐
│                    Scheduler 批量更新机制                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   同步代码块:                                              │
│   ┌──────────────────────────────────────────────────┐     │
│   │  count.value = 1   → trigger(effect)             │     │
│   │  count.value = 2   → trigger(effect)  (重复)      │     │
│   │  count.value = 3   → trigger(effect)  (重复)      │     │
│   └──────────────────────────────────────────────────┘     │
│         │                                                   │
│         │  effect 不会立即执行,而是进入调度队列              │
│         │  scheduler 发现同一个 effect 已在队列中 → 去重      │
│         │                                                   │
│         ▼                                                   │
│   ┌──────────────────────────────────────────────────┐     │
│   │  微任务队列 (Promise.then / queueMicrotask)      │     │
│   │                                                  │     │
│   │  等待当前同步代码全部执行完毕...                    │     │
│   │                                                  │     │
│   │  → 执行 effect(组件仅重新渲染一次!)             │     │
│   └──────────────────────────────────────────────────┘     │
│                                                             │
│   nextTick(() => {                                          │
│     // 在这里可以拿到更新后的 DOM                           │
│     console.log(document.querySelector('#app').textContent) │
│   })                                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

第2部分:ref vs reactive 决策树 ​

2.1 内部实现差异 ​

ref 和 reactive 在底层共用同一套 Proxy 机制,但包装方式不同:

ts
// 概念等价:ref(0) 约等于 reactive({ value: 0 })
// ref 内部维护一个 .value 属性,该属性本身被 reactive 包装

// Vue 3 内部 ref 简化实现
class RefImpl<T> {
  private _value: T
  private _rawValue: T
  public readonly __v_isRef = true

  constructor(value: T) {
    this._rawValue = value
    // 如果是对象,用 reactive 包裹;基本值直接存储
    this._value = isObject(value) ? reactive(value) : value
  }

  get value(): T {
    track(this, 'value')   // 依赖收集
    return this._value
  }

  set value(newVal: T) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = isObject(newVal) ? reactive(newVal) : newVal
      trigger(this, 'value')  // 派发更新
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

2.2 .value 解包规则 ​

Vue 在特定上下文中会自动解包 Ref,减少模板和代码中的 .value 冗余:

┌─────────────────────────────────────────────────────────────┐
│                    .value 自动解包规则                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  模板 (template) 中:                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <div>{{ count }}</div>     ← 自动解包,无需 .value  │   │
│  │  <div>{{ obj.count }}</div> ← 仅顶层 ref 自动解包    │   │
│  │  <div>{{ obj.count + 1 }}</div> ← 解包后参与运算     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  reactive 对象内部:                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  const state = reactive({                           │   │
│  │    count: ref(0),      ← 自动解包!                 │   │
│  │    name: 'hello'                                   │   │
│  │  })                                                │   │
│  │  state.count  // 0  (不是 RefImpl 对象)             │   │
│  │  state.count += 1  // ✅ 直接修改,无需 .value       │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  数组 / Map 等集合类型中:❌ 不会自动解包                    │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  const arr = reactive([ref(1), ref(2)])             │   │
│  │  arr[0]        // RefImpl { value: 1 }  需要 .value │   │
│  │  const map = reactive(new Map([['k', ref(10)]]))    │   │
│  │  map.get('k')  // RefImpl { value: 10 } 需要 .value │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

2.3 决策树 ​

┌─────────────────────────────────────────────────────────────┐
│              ref vs reactive vs shallowRef 决策树            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   你的数据是什么?                                           │
│         │                                                   │
│    ┌────┴────────────────────────────┐                     │
│    ▼                                 ▼                     │
│  基本类型                           对象/数组               │
│  (string/number/boolean)            /Map/Set               │
│    │                                 │                     │
│    ▼                                 ▼                     │
│  使用 ref                  是否需要整体替换?                 │
│  const n = ref(0)           │                              │
│                           ┌──┴──┐                          │
│                           ▼     ▼                          │
│                          是     否                          │
│                           │     │                           │
│                           ▼     ▼                           │
│                        ref   reactive                       │
│                        .value 直接替换  const state =       │
│                        = 新对象  reactive({...})            │
│                                                             │
│  特殊情况:                                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  shallowRef  → 仅 .value 本身是响应式的              │   │
│  │  适用:大型不可变数据(Markdown 内容)、第三方实例     │   │
│  │  const md = shallowRef('# Title')                   │   │
│  │  md.value = newContent  // ✅ 触发更新               │   │
│  │  md.value += '...'      // ❌ 不触发(未修改 .value)│   │
│  │                                                      │   │
│  │  shallowReactive → 仅对象第一层属性是响应式的          │   │
│  │  适用:性能敏感的大对象,只关心顶层属性变化            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ts
// 各 API 适用场景代码示例

// ref —— 基本值、需要整体替换的值
const count = ref(0)
const user = ref<User | null>(null)
user.value = await fetchUser()  // 整体替换,触发更新

// reactive —— 表单、配置对象等聚合数据
const form = reactive({
  username: '',
  password: '',
  rememberMe: false,
})
// 不可解构:const { username } = form  ❌ 失去响应式

// shallowRef —— 大体积不可变数据
import type { MarkdownContent } from '@/types'
const markdownContent = shallowRef<MarkdownContent>({
  raw: '# Hello',
  html: '<h1>Hello</h1>',
  toc: [{ level: 1, text: 'Hello' }],
})
// 通过整体替换 .value 触发更新,避免深层响应式转换的开销

// shallowReactive —— 仅顶层属性需要响应的场景
const chartOptions = shallowReactive({
  width: 800,
  height: 600,
  theme: {       // 深层对象不会被代理
    colors: ['#ff0000', '#00ff00'],
    fonts: { size: 14 },
  },
})
// chartOptions.width = 900  ✅ 触发更新
// chartOptions.theme.colors.push('#0000ff')  ❌ 不触发更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

第3部分:computed 的惰性求值 ​

3.1 依赖追踪与缓存机制 ​

computed 与普通函数的核心区别在于:它只在依赖变化时才重新计算,否则返回上次缓存的结果。这是一个"惰性求值"(lazy evaluation)的过程。

┌─────────────────────────────────────────────────────────────┐
│              computed 惰性求值与缓存流程                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   const fullName = computed(() => {                         │
│     console.log('computed 执行了')                          │
│     return `${firstName.value} ${lastName.value}`           │
│   })                                                        │
│                                                             │
│   ┌──────────────────────────────────────────────────┐     │
│   │  初始状态:_dirty = true (缓存失效)               │     │
│   │                   │                              │     │
│   │  首次读取 fullName.value                          │     │
│   │       │                                          │     │
│   │       ▼                                          │     │
│   │  执行 getter → 计算结果 → 缓存 → _dirty = false  │     │
│   │       │                                          │     │
│   │  再次读取 fullName.value                          │     │
│   │       │                                          │     │
│   │       ▼                                          │     │
│   │  _dirty === false → 直接返回缓存,不执行 getter   │     │
│   │       │                                          │     │
│   │  firstName.value 或 lastName.value 变化           │     │
│   │       │                                          │     │
│   │       ▼                                          │     │
│   │  trigger → _dirty = true (标记缓存失效)           │     │
│   │       │                                          │     │
│   │  下次读取 → 重新执行 getter → 更新缓存             │     │
│   └──────────────────────────────────────────────────┘     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ts
// computed 内部简化实现
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true       // 缓存是否失效
  private _effect: ReactiveEffect

  constructor(getter: () => T) {
    // effect 的调度器:依赖变化时不立即执行,只标记 dirty
    this._effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        trigger(this, 'value')  // 通知读取了此 computed 的 effect
      }
    })
  }

  get value(): T {
    if (this._dirty) {
      this._value = this._effect.run()
      this._dirty = false
    }
    track(this, 'value')  // 收集依赖此 computed 的 effect
    return this._value
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

3.2 computed vs watch + 手动赋值 ​

很多初学者会用 watch 手动将派生值写入 ref,这是反模式:

ts
// ❌ 反模式:用 watch 手动同步派生状态
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = ref('')

watch(
  [firstName, lastName],
  ([first, last]) => {
    fullName.value = `${first} ${last}`  // 手动赋值
  },
  { immediate: true }
)

// ✅ 正确做法:用 computed 表达派生关系
const fullName = computed(() =>
  `${firstName.value} ${lastName.value}`
)

// 为什么更好?
// 1. 声明式:直接表达"fullName 由 firstName + lastName 派生"
// 2. 缓存:依赖未变时不重新计算
// 3. 类型推断:自动推断为 ComputedRef<string>
// 4. 无多余 effect:不产生额外的订阅开销
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

3.3 可写 computed(getter + setter) ​

computed 可以同时提供 getter 和 setter,在需要双向绑定的场景下非常有用:

vue
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{
  firstName: string
  lastName: string
}>()

const emit = defineEmits<{
  'update:firstName': [value: string]
  'update:lastName': [value: string]
}>()

// 可写 computed:将两个 prop 合并为一个逻辑字段
const fullName = computed({
  get: () => `${props.firstName} ${props.lastName}`,
  set: (val: string) => {
    const [first, ...rest] = val.trim().split(' ')
    emit('update:firstName', first)
    emit('update:lastName', rest.join(' '))
  },
})
</script>

<template>
  <input v-model="fullName" placeholder="输入全名" />
  <p>First: {{ firstName }}</p>
  <p>Last: {{ lastName }}</p>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

第4部分:watch 与 watchEffect 的完整对比 ​

4.1 执行时序与 flush 选项 ​

┌─────────────────────────────────────────────────────────────┐
│          watch / watchEffect / watchPostEffect /            │
│          watchSyncEffect 执行时序对比                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  时间轴:                                                   │
│                                                             │
│  ── DOM 更新前(pre flush,默认)──                          │
│  │  watchEffect(() => { ... })                             │
│  │  watch(source, callback)    ← 默认 flush: 'pre'         │
│  │                                                         │
│  ── 组件 DOM 更新 ──                                        │
│  │                                                         │
│  ── DOM 更新后(post flush)──                              │
│  │  watchPostEffect(() => {                                │
│  │    // 可以安全访问更新后的 DOM                            │
│  │  })                                                     │
│  │  watch(source, cb, { flush: 'post' })                  │
│  │                                                         │
│  ── 同步(sync flush)──                                    │
│  │  watchSyncEffect(() => { ... })                         │
│  │  watch(source, cb, { flush: 'sync' })                  │
│  │  ⚠️ 状态变化时立即同步执行,可能多次触发                  │
│  │                                                         │
│  ── nextTick ──                                             │
│  │  nextTick(() => { /* DOM 已保证是最新 */ })             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

4.2 watch vs watchEffect 选择指南 ​

ts
// watchEffect —— 自动收集依赖,适合简单逻辑
const counter = ref(0)
const doubled = ref(0)

// ❌ 不适合 watchEffect:你需要在回调中访问什么?
//    它会在首次立即执行,依赖自动追踪
watchEffect(() => {
  // 首次执行时 counter 为 0,自动建立依赖关系
  doubled.value = counter.value * 2
})

// watch —— 显式指定来源,适合需要旧值/精确控制的场景
// ✅ 适合 watch:
// 1. 需要新旧值对比
watch(counter, (newVal, oldVal) => {
  console.log(`counter: ${oldVal} → ${newVal}`)
})

// 2. 异步操作(网络请求)
watch(
  () => route.params.id,
  async (id, _oldId, onCleanup) => {
    const controller = new AbortController()
    onCleanup(() => controller.abort())
    data.value = await fetchData(String(id), controller.signal)
  },
  { immediate: true }
)

// 3. 精确控制触发条件
watch(
  () => form.value.email,
  (email) => {
    // 只有 email 变化时才执行
  }
)

// 4. immediate + deep 选项
watch(
  () => settings.value,
  (newSettings) => {
    applySettings(newSettings)
  },
  { immediate: true, deep: true }
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

4.3 onCleanup 清理与 stop handle ​

┌─────────────────────────────────────────────────────────────┐
│              watch 副作用清理机制                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  场景:用户快速切换路由 / 输入搜索关键词                      │
│                                                             │
│  第1次 watch 触发 → 发起请求 A                               │
│  第2次 watch 触发 → onCleanup 先执行 → 取消请求 A           │
│                   → 发起请求 B                              │
│  第3次 watch 触发 → onCleanup 先执行 → 取消请求 B           │
│                   → 发起请求 C                              │
│                                                             │
│  最终只有请求 C 的结果会显示,避免竞态条件                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ts
// onCleanup 的两种使用方式

// 方式一:watch 回调的第三个参数
watch(keyword, async (newVal, _oldVal, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())

  results.value = await search(newVal, controller.signal)
})

// 方式二:watchEffect 内部调用 onCleanup
import { watchEffect, onCleanup } from 'vue'
// 注意:onCleanup 在 watchSyncEffect 中不可用

let timer: ReturnType<typeof setInterval>

// stop handle —— 手动停止监听
const stop = watchEffect((onCleanup) => {
  timer = setInterval(() => {
    refreshData()
  }, 5000)

  onCleanup(() => {
    clearInterval(timer)
    console.log('定时器已清理')
  })
})

// 需要时手动停止
onUnmounted(() => {
  stop()  // 停止 watchEffect,触发 onCleanup
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

4.4 watch 的 deep 选项陷阱 ​

ts
const hugeObject = reactive({
  // ... 数千个嵌套属性
})

// ❌ deep: true 会递归遍历所有嵌套属性建立依赖
//    对大对象来说性能代价极高
watch(hugeObject, () => {
  // ...
}, { deep: true })

// ✅ 替代方案:精确监听需要的属性
watch(
  () => hugeObject.specificField.subField,
  (newVal) => { /* ... */ }
)

// ✅ 或用 computed 提取需要的片段
const relevantData = computed(() => ({
  a: hugeObject.deeply.nested.a,
  b: hugeObject.deeply.nested.b,
}))
watch(relevantData, (newVal) => { /* ... */ })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第5部分:可组合函数(Composables)设计模式 ​

5.1 命名约定与返回值设计 ​

┌─────────────────────────────────────────────────────────────┐
│              Composable 设计规范                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  命名:                                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ✅ useXxx     useMouse, useFetch, useLocalStorage  │   │
│  │  ✅ useXxxYyy  useEventListener, useMediaQuery      │   │
│  │  ❌ getXxx     不是 getter                           │   │
│  │  ❌ handleXxx  这是事件处理函数的命名                   │   │
│  │  ❌ initXxx    这是初始化函数的命名                    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  返回值:返回 ref / reactive / computed / 方法               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ✅ 返回 ref —— 调用方保持响应式连接                 │   │
│  │  ✅ 返回 computed —— 只读派生状态                    │   │
│  │  ✅ 返回普通函数 —— 提供修改入口                     │   │
│  │  ❌ 返回解构后的普通值 —— 丢失响应式                 │   │
│  │  ❌ 返回 reactive 对象 —— 调用方解构时丢失响应式     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

5.2 参数标准化:MaybeRef 与 toValue ​

Composable 的输入应该足够灵活,允许调用方传入 ref、getter 或普通值。Vue 3.3+ 提供了 MaybeRef、MaybeRefOrGetter 类型和 toValue 工具函数。

ts
import { ref, computed, toValue, type MaybeRefOrGetter } from 'vue'

// 参数类型:MaybeRefOrGetter<T> = T | Ref<T> | (() => T)

export function useTitle(newTitle: MaybeRefOrGetter<string>) {
  // toValue 统一归一化:Ref 取 .value,getter 调用求值,普通值直接返回
  const title = computed(() => toValue(newTitle))

  watchEffect(() => {
    document.title = title.value
  })

  return title
}

// 使用方式灵活
useTitle('Home')                        // 普通字符串
useTitle(ref('Dashboard'))              // Ref
useTitle(() => `Page ${page.value}`)    // Getter(响应式派生)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

5.3 异步 Composable 模式 ​

ts
// 标准的异步数据获取 Composable
export function useAsyncData<T>(
  fetcher: () => Promise<T>,
) {
  const data = shallowRef<T | null>(null)
  const error = ref<Error | null>(null)
  const isLoading = ref(false)

  async function execute() {
    isLoading.value = true
    error.value = null
    try {
      data.value = await fetcher()
    } catch (e) {
      error.value = e as Error
    } finally {
      isLoading.value = false
    }
  }

  // 返回统一的 { data, error, isLoading, execute } 接口
  return { data, error, isLoading, execute }
}

// 使用
const { data: users, isLoading, execute } = useAsyncData(
  () => fetch('/api/users').then(r => r.json())
)

onMounted(() => execute())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

5.4 effectScope 管理生命周期 ​

当 Composable 内部创建了多个 watch/watcher 时,手动管理每个 stop handle 很繁琐。effectScope 允许批量管理一组 effect 的生命周期。

ts
import { effectScope, onScopeDispose, ref, watchEffect } from 'vue'

export function useFeatureFlag(key: string) {
  const enabled = ref(false)
  const scope = effectScope()  // 创建独立的作用域

  scope.run(() => {
    // 此 scope 内的所有 effect 都会被统一管理
    const stored = localStorage.getItem(`flag:${key}`)
    enabled.value = stored === 'true'

    watchEffect(() => {
      localStorage.setItem(`flag:${key}`, String(enabled.value))
    })

    // onScopeDispose 注册清理回调
    onScopeDispose(() => {
      console.log(`Feature flag "${key}" scope disposed`)
    })
  })

  function dispose() {
    scope.stop()  // 一次性停止所有 effect
  }

  return { enabled, dispose }
}

// 在组件中使用
const { enabled: showNewUI, dispose } = useFeatureFlag('new-dashboard')
onUnmounted(dispose)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

5.5 provide/inject 与 Composable 配合 ​

┌─────────────────────────────────────────────────────────────┐
│          provide/inject + Composable 模式                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌──────────────────────────────────────────────────┐     │
│   │  父组件                                          │     │
│   │  ┌───────────────────────────────────────────┐  │     │
│   │  │  useTheme()          ← 内部 provide()     │  │     │
│   │  │  const theme = ref('light')               │  │     │
│   │  │  provide(THEME_KEY, readonly(theme))      │  │     │
│   │  │  return { theme, toggle }                 │  │     │
│   │  └───────────────────────────────────────────┘  │     │
│   │              │  inject(THEME_KEY)                │     │
│   │  ┌───────────┴────────────────────────────────┐ │     │
│   │  │  子组件                                    │ │     │
│   │  │  useTheme()  ← 内部 inject()               │ │     │
│   │  │  const theme = inject(THEME_KEY)           │ │     │
│   │  │  return { theme }                           │ │     │
│   │  └────────────────────────────────────────────┘ │     │
│   └──────────────────────────────────────────────────┘     │
│                                                             │
│  关键原则:                                                  │
│  • 用 Symbol 作为注入键,避免命名冲突                       │
│  • 修改状态的函数由 Provider 暴露,消费者只读               │
│  • 提供默认值防止缺失 Provider 时崩溃                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ts
// 完整示例:主题管理的 provide/inject + Composable

// theme.ts —— 键定义 + Composable 函数
import { ref, readonly, inject, provide, type InjectionKey, type Ref } from 'vue'

type Theme = 'light' | 'dark'

export const THEME_KEY: InjectionKey<{
  theme: Ref<Theme>
  toggle: () => void
}> = Symbol('theme')

export function useThemeProvider() {
  const theme = ref<Theme>('light')
  const toggle = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  provide(THEME_KEY, { theme: readonly(theme), toggle })
  return { theme, toggle }
}

export function useTheme() {
  const ctx = inject(THEME_KEY)
  if (!ctx) {
    throw new Error('useTheme() must be used within a <ThemeProvider>')
  }
  return ctx
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

第6部分:Vue 3.3 / 3.4 / 3.5 新特性 ​

6.1 Vue 3.3:defineModel(v-model 简化) ​

在 Vue 3.3 之前,实现组件的 v-model 需要手动声明 props 和 emits。defineModel 将两者合并,大幅减少样板代码。

vue
<!-- Vue 3.3 之前 -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

const localValue = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val),
})
</script>

<!-- Vue 3.3+ defineModel -->
<script setup lang="ts">
// 一个宏替代了 props + emits + computed 三件套
const modelValue = defineModel<string>({ required: true })

// 命名 v-model (v-model:title)
const title = defineModel<string>('title')

// 带默认值
const count = defineModel<number>({ default: 0 })
</script>

<template>
  <input v-model="modelValue" />
  <input v-model="title" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

6.2 Vue 3.5:useTemplateRef(类型安全模板引用) ​

Vue 3.5 引入 useTemplateRef,替代传统的 ref(null) 方式获取模板引用,提供更好的类型推断。

vue
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'

// Vue 3.4 及之前
// const inputRef = ref<HTMLInputElement | null>(null)

// Vue 3.5+ 类型安全模板引用
const inputRef = useTemplateRef<HTMLInputElement>('inputEl')

onMounted(() => {
  // 类型自动推断为 HTMLInputElement | null
  inputRef.value?.focus()
})
</script>

<template>
  <input ref="inputEl" placeholder="类型安全引用" />
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6.3 Vue 3.3:defineSlots(类型安全插槽) ​

vue
<script setup lang="ts">
// 声明组件暴露的插槽及其类型
defineSlots<{
  default?: (props: { item: { id: number; name: string } }) => unknown
  header?: () => unknown
  footer?: (props: { count: number }) => unknown
}>()
</script>

<template>
  <header><slot name="header" /></header>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" />
    </li>
  </ul>
  <footer><slot name="footer" :count="items.length" /></footer>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6.4 toValue / toRef / toRefs 工具函数 ​

┌─────────────────────────────────────────────────────────────┐
│          toValue / toRef / toRefs 功能对照                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  toValue(value) —— Vue 3.3+                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  归一化:ref → 取 .value                             │   │
│  │          getter → 调用求值                           │   │
│  │          普通值 → 直接返回                           │   │
│  │  非响应式:返回的是快照值                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  toRef(reactiveObj, 'key') —— 保持响应式连接                │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  从 reactive 对象中提取单个属性为 ref                │   │
│  │  修改 ref 的值会同步回源对象                         │   │
│  │  const state = reactive({ x: 1 })                   │   │
│  │  const xRef = toRef(state, 'x')                     │   │
│  │  xRef.value = 10  // state.x 也变为 10              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  toRefs(reactiveObj) —— 解构保持响应式                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  将 reactive 对象的每个属性转为 ref                  │   │
│  │  const { x, y } = toRefs(state)                     │   │
│  │  // x 和 y 都是 Ref,解构后不丢失响应式              │   │
│  │  // 常用于 Composable 返回值中解构 reactive 对象     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

第7部分:Vue 2 Options API 到 Vue 3 Composition API 迁移 ​

7.1 概念对照表 ​

┌─────────────────────────────────────────────────────────────┐
│        Vue 2 Options API → Vue 3 Composition API 映射表       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Vue 2 (Options API)        │  Vue 3 (Composition API)      │
│  ───────────────────────────┼────────────────────────────── │
│  data() { return { x } }    │  const x = ref(initial)       │
│  computed: { fullName() }   │  const fullName = computed()  │
│  methods: { doSmth() }      │  function doSmth() { }        │
│  watch: { x(val, old) }     │  watch(x, (val, old) => {})   │
│  mounted()                  │  onMounted(() => {})          │
│  updated()                  │  onUpdated(() => {})          │
│  unmounted()                │  onUnmounted(() => {})        │
│  beforeDestroy()            │  onBeforeUnmount(() => {})    │
│  props: ['title']           │  defineProps<{ title }>()     │
│  this.$emit('event', val)   │  emit('event', val)           │
│  mixins: [myMixin]          │  useMyMixin() ← Composable    │
│  provide: { key: val }      │  provide(KEY, val)            │
│  inject: ['key']            │  inject(KEY)                  │
│  template refs: this.$refs  │  useTemplateRef('el')         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

7.2 setup() 函数 vs <script setup> ​

vue
<!-- Vue 3 早期:setup() 函数 -->
<script lang="ts">
import { ref, computed, onMounted } from 'vue'

export default {
  props: { initialCount: Number },
  emits: ['update'],
  setup(props, { emit }) {
    const count = ref(props.initialCount)
    const doubled = computed(() => count.value * 2)

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

    onMounted(() => {
      console.log('mounted')
    })

    // 必须 return 才能在模板中使用
    return { count, doubled, increment }
  },
}
</script>

<!-- Vue 3.2+:<script setup>(推荐) -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const props = defineProps<{ initialCount: number }>()
const emit = defineEmits<{ 'update': [value: number] }>()

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

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

onMounted(() => {
  console.log('mounted')
})
// 顶层变量和函数自动暴露给模板,无需 return
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

7.3 mixins → composables 的思维转换 ​

┌─────────────────────────────────────────────────────────────┐
│              mixins → composables 思维转换                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  mixins 的问题:                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ❌ 来源不明:this.method() 来自哪个 mixin?         │   │
│  │  ❌ 命名冲突:多个 mixin 的同名属性静默覆盖          │   │
│  │  ❌ 隐式耦合:mixin 依赖 this 上由别的 mixin 提     │   │
│  │      供的属性,运行时才发现缺失                      │   │
│  │  ❌ 不可组合:mixin 之间不能互相调用                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  composables 的优势:                                       │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ✅ 来源明确:const mouse = useMouse() 一眼看出      │   │
│  │  ✅ 显式命名:解构时可重命名 const { x: mx } = ...  │   │
│  │  ✅ 类型安全:完整 TypeScript 支持                   │   │
│  │  ✅ 可组合:composable 内部可以调用其他 composable   │   │
│  │  ✅ 作用域隔离:每次调用创建独立的状态副本           │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ts
// Vue 2 mixin 示例
const paginationMixin = {
  data() {
    return { page: 1, pageSize: 20 }
  },
  methods: {
    nextPage() { this.page++ },
    prevPage() { this.page-- },
  },
}

// ------------------- 迁移为 Composable ------------------- //

// Vue 3 Composable 等价实现
export function usePagination(initialPage = 1, initialPageSize = 20) {
  const page = ref(initialPage)
  const pageSize = ref(initialPageSize)
  const offset = computed(() => (page.value - 1) * pageSize.value)

  function nextPage() { page.value++ }
  function prevPage() { if (page.value > 1) page.value-- }
  function resetPage() { page.value = 1 }

  // 可选:监听 page 变化自动重新获取数据
  // watch(page, () => fetchPageData())

  return { page, pageSize, offset, nextPage, prevPage, resetPage }
}

// 组件中使用
const { page, pageSize, offset, nextPage, prevPage } = usePagination()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

7.4 常见迁移陷阱 ​

ts
// 陷阱一:this 的消失
// Vue 2
// this.$router.push('/home')
// this.$store.dispatch('login')
// Vue 3
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/home')
// store 同理:import { useStore } from 'vuex' 或 Pinia

// 陷阱二:$refs 替换
// Vue 2: this.$refs.myInput.focus()
// Vue 3.4-: const myInput = ref<HTMLInputElement | null>(null)
// Vue 3.5+: const myInput = useTemplateRef<HTMLInputElement>('myInput')

// 陷阱三:filters 已移除
// Vue 2: {{ amount | currency }}
// Vue 3: const formatted = computed(() => formatCurrency(amount.value))
//     或注册全局属性 app.config.globalProperties.$filters

// 陷阱四:事件总线 ($on/$off/$once) 已移除
// 替代方案:mitt 库、provide/inject、Pinia store

// 陷阱五:Vue 2 的 Vue.set / Vue.delete
// Vue 2: Vue.set(obj, 'newProp', value)
// Vue 3: obj.newProp = value  // Proxy 自动检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

核心总结 ​

总结1:响应式系统核心架构 ​

Proxy (拦截 get/set/deleteProperty)
  ├── get()    → track(target, key)   → 收集依赖(effect → 属性映射)
  ├── set()    → trigger(target, key) → 派发更新(查找依赖 → 调度执行)
  └── Scheduler → 批量去重 → 微任务队列 → 组件只渲染一次
1
2
3
4

总结2:API 选择速查 ​

需求API说明
基本类型 / 需要整体替换ref首选,模板自动解包
表单对象 / 聚合状态reactive不可解构,深层响应
大型不可变数据shallowRef仅 .value 触发更新
顶层属性响应即可shallowReactive避免深层代理开销
派生值computed惰性求值,自动缓存
需要旧值 / 异步操作watch显式来源,支持清理
简单同步协议watchEffect自动收集依赖

总结3:Composable 设计 Checklist ​

  • 函数名以 use 开头
  • 参数支持 MaybeRefOrGetter<T> + toValue() 归一化
  • 返回值用 ref / computed(不用解构后的普通值)
  • 副作用在 onMounted / onUnmounted 中启动和清理
  • 复杂的 effect 集合用 effectScope 统一管理
  • SSR 安全:不在模块顶层保存用户级可变状态
  • 用 Symbol 作为 provide/inject 的键

总结4:迁移要点 ​

  • data / computed / methods → ref / computed / 普通函数
  • mounted / destroyed → onMounted / onUnmounted
  • mixins → composables(显式、类型安全、无冲突)
  • this.$xxx → useXxx() 组合式函数
  • filters → 手动计算属性或函数调用
  • <script setup> 替代 setup() 函数(减少样板代码)

章节测试 ​

测试1:响应式原理 ​

请描述 Vue 3 中 track 和 trigger 分别做什么,以及它们是在哪些 Proxy trap 中被调用的。

测试2:ref vs reactive ​

以下代码中,哪些修改会触发视图更新?为什么?

ts
const state = reactive({ items: [1, 2, 3] })
state.items.push(4)           // ①
state.items = [4, 5, 6]       // ②
const { items } = state
items.push(7)                 // ③
const itemsRef = toRef(state, 'items')
itemsRef.value.push(8)        // ④
1
2
3
4
5
6
7

测试3:computed 缓存 ​

ts
const a = ref(1)
const b = ref(2)
const sum = computed(() => {
  console.log('computed')
  return a.value + b.value
})

console.log(sum.value)   // 输出 "computed" + 3
console.log(sum.value)   // 输出 3(无 "computed")
a.value++
console.log(sum.value)   // 输出什么?解释原因。
1
2
3
4
5
6
7
8
9
10
11

测试4:watch 清理 ​

在以下代码中,用户连续快速输入 "a" → "ab" → "abc",最终 results 的值会有竞态问题吗?为什么?

ts
watch(keyword, async (val, _old, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  const data = await fetch(`/api/search?q=${val}`, {
    signal: controller.signal,
  }).then(r => r.json())
  results.value = data
})
1
2
3
4
5
6
7
8

测试5:Composable 设计 ​

以下 Composable 有什么问题?请指出并修复。

ts
export function useCounter() {
  const state = reactive({ count: 0 })
  const doubled = computed(() => state.count * 2)
  return { ...state, doubled }
}
1
2
3
4
5

测试6:迁移 ​

将以下 Vue 2 mixin 改写为 Vue 3 Composable:

ts
const formMixin = {
  data() {
    return { loading: false, error: null }
  },
  methods: {
    async submit(data) {
      this.loading = true
      try {
        await this.$axios.post('/api/form', data)
      } catch (e) {
        this.error = e.message
      } finally {
        this.loading = false
      }
    },
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

参考答案 ​

测试1答案 ​

答案:

  • track(依赖收集):在 Proxy 的 get trap 中被调用。当读取响应式对象的属性时,track 将当前正在执行的 effect(如组件的渲染函数)注册为该属性的订阅者。数据结构为 targetMap: WeakMap<target, Map<key, Set<effect>>>。

  • trigger(派发更新):在 Proxy 的 set、deleteProperty 等 trap 中被调用。当修改/删除属性时,trigger 查找所有订阅了该属性的 effect 并通知它们重新执行(或通过 scheduler 调度)。


测试2答案 ​

答案:

  • ① ✅ 触发更新 —— items 是 Proxy 代理的数组,push 被 Proxy 拦截并触发 trigger
  • ② ✅ 触发更新 —— 整体替换 items 属性,set trap 触发 trigger
  • ③ ❌ 不触发更新 —— const { items } = state 解构得到的是 Proxy 代理的数组引用(快照),修改这个数组虽然内部元素会变,但如果后续组件不访问 state.items 就不会重新渲染。实际上 items.push(7) 操作的是同一个 Proxy 数组,所以 会触发更新 — 因为 Proxy 数组是同一个引用。但如果是解构基础属性(如 const { count } = reactive({ count: 0 })),则 count 是普通值,修改不会触发。
  • ④ ✅ 触发更新 —— toRef 保持了与源对象的响应式连接,修改 value 等价于修改 state.items

测试3答案 ​

答案:

输出 "computed" + undefined(实际输出 "computed" + 3)。

解析:a.value++ 将 a 从 1 改为 2,这会触发 sum 的依赖更新通知——将 _dirty 标记为 true。但 computed 是惰性求值的,getter 不会在 a.value++ 时立即执行,而是等到下次读取 sum.value 时才执行。所以第三次 console.log 会输出 "computed" + 4(1+2=3 是旧的;此时 a=2, b=2, 重新计算得 4)。

更正:第一次输出 computed + 3;第二次输出 3(无 computed);a = 2, b = 2;第三次输出 computed + 4。


测试4答案 ​

答案:不会有竞态问题。

解析:每次 watch 回调执行前,Vue 会先调用上一次的 onCleanup 回调。用户输入 "a"→"ab"→"abc" 的过程中:

  1. 输入 "a" → 创建 AbortController A → 发起请求 A
  2. 输入 "ab" → 先执行 onCleanup → controller.abort() 取消请求 A → 创建 AbortController B → 发起请求 B
  3. 输入 "abc" → 先执行 onCleanup → controller.abort() 取消请求 B → 创建 AbortController C → 发起请求 C

只有请求 C 的结果会写入 results.value。已被 abort 的请求会抛出 AbortError,但该错误被浏览器静默处理,不会污染 results。


测试5答案 ​

答案:return { ...state, doubled } 中展开 state 会丢失响应式。

...state 将 reactive 对象的属性复制为普通值(count: 0 而非 Ref<number>)。调用方拿到的是一个不会触发更新的普通数值。

修复:

ts
export function useCounter() {
  const count = ref(0)  // 使用 ref 而非 reactive
  const doubled = computed(() => count.value * 2)
  return { count, doubled }
}
// 调用方:const { count, doubled } = useCounter()
// count 是 Ref<number>,template 中自动解包,script 中用 .value
1
2
3
4
5
6
7

或使用 toRefs:

ts
export function useCounter() {
  const state = reactive({ count: 0 })
  const doubled = computed(() => state.count * 2)
  return { ...toRefs(state), doubled }
}
1
2
3
4
5

测试6答案 ​

答案:

ts
export function useFormSubmit(url: string) {
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function submit(data: unknown) {
    loading.value = true
    error.value = null
    try {
      return await $fetch(url, { method: 'POST', body: data })
    } catch (e) {
      error.value = (e as Error).message
      throw e  // 让调用方也能捕获
    } finally {
      loading.value = false
    }
  }

  return { loading, error, submit }
}

// 使用:
const { loading, error, submit } = useFormSubmit('/api/form')
await submit({ name: 'Alice' })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

相关笔记 ​

  • [[01-vue-components-and-templates]] - 组件基础与模板语法
  • [[04-pinia-state-management]] - Pinia 状态管理
  • [[../02-react-and-nextjs/00-overview]] - React 生态对比
  • [[04-vue-router]] - Vue Router 路由管理

下一步学习 ​

  • [ ] 阅读 04 - 状态管理与 Pinia
  • [ ] Vue 官方文档:Reactivity in Depth
  • [ ] Vue 官方文档:Composables

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇2. Vue 组件、模板与编译原理 / Vue Components, Templates, and Compilation
下一篇4. Vue Router、表单与组件架构 / Vue Router, Forms, and Component Architecture

持续记录,持续成长

Copyright © Tidenflow