响应式系统概述

Vue3 的响应式系统是整个框架的核心,它基于 Proxy 实现,相比 Vue2 的 Object.defineProperty,具有更强大的拦截能力和更好的性能表现。

什么是响应式?

响应式的核心思想是:当数据发生变化时,依赖这些数据的地方(如视图、计算属性、侦听器)能够自动更新

// 期望的效果
const state = reactive({ count: 0 })

// 当 count 变化时,视图自动更新
state.count++ // 自动触发组件重新渲染

Vue3 响应式系统的三大核心

1️⃣ Proxy 代理层(基础)
  • 使用 Proxy 拦截对象的读取(get)和修改(set)操作

  • 实现惰性深层代理:嵌套对象只在访问时才创建 Proxy,优化性能

2️⃣ 依赖收集层(订阅-发布)
  • track(订阅):在访问属性时,记录"哪个 effect 依赖了哪个属性"

  • trigger(发布):在修改属性时,触发所有依赖该属性的 effect 执行

3️⃣ 应用层(effect/computed/watch)
  • effect:副作用函数,是响应式系统的执行单元

  • computed:基于 effect 实现,具有缓存 + dirty 机制

  • watch:基于 effect 实现,用于监听数据变化并执行回调

  • 组件渲染:组件的渲染函数本身就是一个 effect

核心数据结构

// 全局依赖映射表
const targetMap = new WeakMap<object, Map<any, Set<ReactiveEffect>>>()

/**
 * 结构示意:
 * targetMap: WeakMap {
 *   obj1: Map {
 *     'count': Set { effect1, effect2 },
 *     'name': Set { effect3 }
 *   },
 *   obj2: Map {
 *     'age': Set { effect4 }
 *   }
 * }
 */

解释

  • WeakMap:键是原始对象,值是该对象的依赖 Map(弱引用,对象销毁时自动清理)

  • Map:键是对象的属性名,值是依赖该属性的 effect 集合

  • Set:存储所有依赖该属性的 effect,用 Set 自动去重

响应式系统的执行流程

┌─────────────────────────────────────────────────────────────┐
│                    创建响应式对象                            │
│                  reactive(obj) / ref(val)                   │
└───────────────────────────┬─────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│              Proxy 代理(拦截 get/set)                      │
│   • 访问属性 → get 拦截 → track 收集依赖                      │
│   • 修改属性 → set 拦截 → trigger 触发更新                    │
│   • 嵌套对象 → 惰性创建 Proxy(按需代理)                      │
└───────────────────────────┬─────────────────────────────────┘
                            │
            ┌───────────────┴───────────────┐
            │                               │
            ▼                               ▼
    ┌──────────────┐              ┌──────────────┐
    │  track 订阅  │              │ trigger 发布  │
    │ 收集依赖关系  │              │  触发副作用   │
    └──────┬───────┘              └──────┬───────┘
           │                              │
           ▼                              ▼
    ┌──────────────┐              ┌──────────────┐
    │ 记录到 targetMap│             │ 从 targetMap  │
    │ 建立属性-effect │             │ 获取 effect   │
    │ 的映射关系      │             │ 并执行        │
    └──────────────┘              └──────┬───────┘
                                         │
                    ┌────────────────────┼────────────────────┐
                    │                    │                    │
                    ▼                    ▼                    ▼
            ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
            │ 组件渲染 effect│    │ computed     │    │ watch        │
            │ diff 更新 DOM │    │ dirty = true │    │ 执行回调      │
            └──────────────┘    └──────────────┘    └──────────────┘

核心机制:Proxy 代理

reactive 的实现原理

reactive 函数接收一个普通对象,返回一个 Proxy 代理对象,拦截对象的各种操作。

function reactive<T extends object>(target: T): T {
  // 如果已经是 Proxy,直接返回
  if (target.__v_isReactive) {
    return target
  }
  
  return new Proxy(target, {
    // 读取属性时触发
    get(target, key, receiver) {
      // 特殊标记
      if (key === '__v_isReactive') return true
      
      const result = Reflect.get(target, key, receiver)
      
      // 依赖收集(订阅)
      track(target, key)
      
      // 🔥 惰性深层代理:嵌套对象在访问时才创建 Proxy
      if (isObject(result)) {
        return reactive(result)
      }
      
      return result
    },
    
    // 修改属性时触发
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 值发生变化时,触发更新(发布)
      if (oldValue !== value) {
        trigger(target, key)
      }
      
      return result
    },
    
    // 删除属性时触发
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    },
    
    // has、ownKeys 等其他拦截...
  })
}

惰性深层代理(Lazy Deep Proxy)

这是 Vue3 响应式系统的重要优化,相比于初始化时递归代理所有嵌套对象,Vue3 采用按需代理策略。

传统方式(Vue2 / 初始化全递归)
// ❌ 性能问题:初始化时递归处理所有嵌套属性
function reactiveOld(obj) {
  Object.keys(obj).forEach(key => {
    if (isObject(obj[key])) {
      obj[key] = reactiveOld(obj[key]) // 递归处理
    }
    Object.defineProperty(obj, key, {
      get() { /*...*/ },
      set() { /*...*/ }
    })
  })
  return obj
}

const state = reactiveOld({
  user: {
    profile: {
      address: {
        city: 'Beijing' // 初始化时就全部处理,即使永远不访问
      }
    }
  }
})

Vue3 方式(惰性代理)
// ✅ 优化:只在访问嵌套属性时才创建 Proxy
const state = reactive({
  user: {
    profile: {
      address: {
        city: 'Beijing'
      }
    }
  }
})

// 1. 初始化:只对 state 本身创建 Proxy
console.log(state) // Proxy { user: { profile: {...} } }

// 2. 访问 state.user 时:触发 get 拦截,才对 user 创建 Proxy
console.log(state.user) // 此时 user 才变成 Proxy

// 3. 访问 state.user.profile 时:才对 profile 创建 Proxy
console.log(state.user.profile) // 此时 profile 才变成 Proxy

优势

  • 性能优化:嵌套层级深但未访问的对象,不会创建 Proxy

  • 💾 内存节省:减少不必要的 Proxy 对象创建

  • 🎯 按需处理:只对真正访问的路径创建响应式

实际案例分析

const state = reactive({
  a: { b: { c: 1 } },
  x: { y: { z: 2 } }
})

// 场景 1:只访问 a.b.c
effect(() => {
  console.log(state.a.b.c)
})
// ✅ 只有 state、state.a、state.a.b 被创建为 Proxy
// ❌ state.x、state.x.y 不会被代理(未访问)

// 场景 2:稍后访问 x.y.z
effect(() => {
  console.log(state.x.y.z)
})
// ✅ 此时 state.x、state.x.y 才被创建为 Proxy

Proxy vs Object.defineProperty

特性 Proxy(Vue3) Object.defineProperty(Vue2)
新增/删除属性 ✅ 自动响应 ❌ 需要 $set/$delete
数组索引 ✅ 直接支持 ❌ 需要重写数组方法
嵌套对象 ✅ 惰性代理 ❌ 初始化全递归
拦截能力 ✅ 13 种操作 ❌ 仅 get/set
性能 ✅ 更好(惰性) ❌ 初始化慢
兼容性 ⚠️ IE11 不支持 ✅ 全兼容

ref 的特殊处理

ref 用于基本类型的响应式,本质是一个包装对象:

function ref<T>(value: T) {
  return {
    _value: value,
    get value() {
      track(this, 'value') // 收集依赖
      return this._value
    },
    set value(newVal) {
      if (newVal !== this._value) {
        this._value = newVal
        trigger(this, 'value') // 触发更新
      }
    }
  }
}

// 使用
const count = ref(0)
effect(() => {
  console.log(count.value) // 通过 .value 访问
})
count.value++ // 触发更新

.value 为什么需要 ?

  • JavaScript 无法拦截基本类型(number、string)的读写

  • 必须包装成对象,通过访问对象属性来触发 get/set

依赖收集:track 订阅

track 的核心作用

当访问响应式对象的属性时,track 函数负责记录当前正在执行的 effect 与该属性之间的依赖关系

// 全局变量:当前正在执行的 effect
let activeEffect: ReactiveEffect | undefined

// 依赖收集函数
function track(target: object, key: string | symbol) {
  // 如果没有 effect 在执行,不需要收集依赖
  if (!activeEffect) return
  
  // 1. 获取该对象的依赖 Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  
  // 2. 获取该属性的依赖 Set
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  
  // 3. 将当前 effect 添加到依赖集合中
  dep.add(activeEffect)
  
  // 4. 反向记录:effect 也需要记录自己依赖了哪些属性(用于清理)
  activeEffect.deps.push(dep)
}

完整的依赖收集流程

const state = reactive({ count: 0, name: 'Vue' })

effect(() => {
  console.log(state.count) // 访问 count
})

/**
 * 执行流程:
 * 
 * 1. effect 函数执行时:
 *    activeEffect = currentEffect
 * 
 * 2. 访问 state.count 时:
 *    触发 Proxy 的 get 拦截
 *    → 调用 track(state, 'count')
 * 
 * 3. track 内部建立映射:
 *    targetMap: WeakMap {
 *      state: Map {
 *        'count': Set { currentEffect }
 *      }
 *    }
 * 
 * 4. effect 执行完毕:
 *    activeEffect = undefined
 */

依赖收集的时机

const state = reactive({ 
  count: 0, 
  name: 'Vue',
  nested: { age: 18 }
})

effect(() => {
  if (state.count > 0) {
    console.log(state.name)      // ✅ 收集 name 的依赖
  }
  console.log(state.nested.age)  // ✅ 收集 nested、nested.age 的依赖
})

// 依赖关系:
// state.count → effect
// state.name → effect(仅当 count > 0)
// state.nested → effect
// state.nested.age → effect

避免重复收集

同一个 effect 对同一个属性只需要收集一次:

effect(() => {
  console.log(state.count)
  console.log(state.count) // 同一 effect 多次访问同一属性
  console.log(state.count)
})

// ✅ Set 自动去重,effect 只会被收集一次
// targetMap.get(state).get('count') === Set { effect }(只有一个)

依赖清理机制

每次 effect 重新执行前,需要清理旧的依赖关系,避免内存泄漏:

const state = reactive({ ok: true, a: 1, b: 2 })

effect(() => {
  // 根据条件收集不同的依赖
  if (state.ok) {
    console.log(state.a) // 收集 a
  } else {
    console.log(state.b) // 收集 b
  }
})

/**
 * 首次执行:state.ok = true
 * → 收集依赖:ok, a
 * 
 * 修改 state.ok = false,effect 重新执行:
 * → 清理旧依赖:ok, a
 * → 收集新依赖:ok, b
 * 
 * 为什么要清理?
 * 如果不清理,state.a 仍会保留对 effect 的引用
 * 修改 state.a 会错误地触发 effect(但此时 effect 不再依赖 a)
 */

派发更新:trigger 发布

trigger 的核心作用

当修改响应式对象的属性时,trigger 函数负责找到所有依赖该属性的 effect 并执行它们

function trigger(target: object, key: string | symbol) {
  // 1. 获取该对象的依赖 Map
  const depsMap = targetMap.get(target)
  if (!depsMap) return // 没有被依赖,直接返回
  
  // 2. 获取该属性的依赖 Set
  const dep = depsMap.get(key)
  if (!dep) return // 该属性没有被依赖
  
  // 3. 执行所有依赖该属性的 effect
  const effects = new Set(dep) // 复制一份,避免执行过程中修改原 Set
  
  effects.forEach(effect => {
    // 避免无限循环:如果当前正在执行的 effect - 执行

#### 3️⃣ **应用层**(effect/computed/watch)

- **effect**:副作用函数,是响应式系统的执行单元

- **computed**:基于 effect 实现,具有缓存 + dirty 机制

- **watch**:基于 effect 实现,用于监听数据变化并执行回调

- **组件渲染**:组件的渲染函数本身就是一个 effect

### 核心数据结构

```typescript
// 全局依赖映射表
const targetMap = new WeakMap<object, Map<any, Set<ReactiveEffect>>>()

/**
 * 结构示意:
 * targetMap: WeakMap {
 *   obj1: Map {
 *     'count': Set { effect1, effect2 },
 *     'name': Set { effect3 }
 *   },
 *   obj2: Map {
 *     'age': Set { effect4 }
 *   }
 * }
 */

解释

  • WeakMap:键是原始对象,值是该对象的依赖 Map(弱引用,对象销毁时自动清理)

  • Map:键是对象的属性名,值是依赖该属性的 effect 集合

  • Set:存储所有依赖该属性的 effect,用 Set 自动去重

响应式系统的执行流程

┌─────────────────────────────────────────────────────────────┐
│                    创建响应式对象                              │
│                  reactive(obj) / ref(val)                    │
└───────────────────────────┬─────────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────────┐
│              Proxy 代理(拦截 get/set)                        │
│   • 访问属性 → get 拦截 → track 收集依赖                        │
│   • 修改属性 → set 拦截 → trigger 触发更新                      │
│   • 嵌套对象 → 惰性创建 Proxy(按需代理)                        │
└───────────────────────────┬─────────────────────────────────┘
                            │
            ┌───────────────┴───────────────┐
            │                               │
            ▼                               ▼
    ┌──────────────┐              ┌──────────────┐
    │  track 订阅   │              │ trigger 发布  │
    │ 收集依赖关系   │              │  触发副作用    │
    └──────┬───────┘              └──────┬───────┘
           │                              │
           ▼                              ▼
    ┌──────────────┐              ┌──────────────┐
    │ 记录到 targetMap│             │ 从 targetMap  │
    │ 建立属性-effect │             │ 获取 effect   │
    │ 的映射关系      │             │ 并执行        │
    └──────────────┘              └──────┬───────┘
                                         │
                    ┌────────────────────┼────────────────────┐
                    │                    │                    │
                    ▼                    ▼                    ▼
            ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
            │ 组件渲染 effect│    │ computed     │    │ watch        │
            │ diff 更新 DOM │    │ dirty = true │    │ 执行回调      │
            └──────────────┘    └──────────────┘    └──────────────┘

effect 副作用系统

effect 是什么?

effect 是 Vue3 响应式系统的执行单元,所有的响应式更新都基于 effect 实现。

class ReactiveEffect {
  active = true
  deps: Set<ReactiveEffect>[] = [] // 该 effect 依赖的所有属性集合
  
  constructor(
    public fn: () => void,           // 副作用函数
    public scheduler?: (effect: ReactiveEffect) => void // 调度器
  ) {}
  
  run() {
    if (!this.active) {
      return this.fn() // 如果 effect 已停止,直接执行不收集依赖
    }
    
    try {
      activeEffect = this // 设置当前活动 effect
      cleanupEffect(this) // 清理旧依赖
      return this.fn()    // 执行副作用函数(会触发依赖收集)
    } finally {
      activeEffect = undefined // 清空当前 effect
    }
  }
  
  stop() {
    if (this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run() // 立即执行一次
  
  // 返回 runner 函数,可以手动触发
  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}

effect 的执行流程

const state = reactive({ count: 0 })

const runner = effect(() => {
  console.log('count:', state.count)
})

/**
 * 执行流程:
 * 
 * 1. 创建 ReactiveEffect 实例
 * 2. 立即执行 effect.run()
 *    - activeEffect = effect
 *    - cleanupEffect(effect) 清理旧依赖
 *    - 执行 fn() → 访问 state.count
 *    - 触发 track(state, 'count')
 *    - 建立依赖关系
 *    - activeEffect = undefined
 * 3. 返回 runner 函数
 * 
 * 修改 state.count = 1:
 * 1. 触发 trigger(state, 'count')
 * 2. 找到依赖的 effect
 * 3. 执行 effect.run()
 * 4. 重新收集依赖
 */

停止 effect

const state = reactive({ count: 0 })

const runner = effect(() => {
  console.log(state.count)
})

// 停止 effect
runner.effect.stop()

// 修改 count,不会触发 effect
state.count++ // 不会输出

effect 的依赖清理

function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect) // 从每个依赖集合中移除自己
    }
    deps.length = 0 // 清空依赖数组
  }
}

为什么要清理?

const state = reactive({ ok: true, a: 1, b: 2 })

effect(() => {
  if (state.ok) {
    console.log(state.a)
  } else {
    console.log(state.b)
  }
})

// 首次:收集 ok, a
// state.ok = false 后重新执行:应该收集 ok, b
// 如果不清理,state.a 仍会错误地触发 effect

computed 计算属性

computed 的核心机制

computed 基于 effect 实现,但具有缓存 + dirty 机制,只有在依赖变化且被访问时才重新计算。

class ComputedRefImpl {
  private _value: any
  private _dirty = true  // 脏值标记
  public readonly effect: ReactiveEffect
  
  constructor(getter: () => any) {
    // 创建 effect,但使用调度器而非立即执行
    this.effect = new ReactiveEffect(getter, () => {
      // 依赖变化时,只标记 dirty,不立即计算
      if (!this._dirty) {
        this._dirty = true
        trigger(this, 'value') // 通知依赖 computed 的 effect
      }
    })
  }
  
  get value() {
    // 收集依赖该 computed 的 effect
    track(this, 'value')
    
    // 只有 dirty 为 true 时才重新计算
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run() // 执行计算函数
    }
    
    return this._value // 返回缓存值
  }
}

function computed(getter: () => any) {
  return new ComputedRefImpl(getter)
}

computed 的完整流程

const state = reactive({ count: 0 })

const double = computed(() => {
  console.log('计算中...')
  return state.count * 2
})

console.log(double.value) // 输出:计算中... 0
console.log(double.value) // 直接返回缓存 0(不输出"计算中...")

state.count = 1 // 只标记 dirty = true,不计算

console.log(double.value) // 输出:计算中... 2(重新计算)
console.log(double.value) // 直接返回缓存 2

执行流程详解

1. 创建 computed:
   - 创建 ReactiveEffect,传入 getter 和 scheduler
   - dirty = true

2. 首次访问 double.value:
   - track(double, 'value') 收集依赖
   - dirty === true,执行 effect.run()
     → 执行 getter() → 访问 state.count
     → track(state, 'count') 建立 effect 与 count 的依赖
   - 缓存结果 _value = 0
   - dirty = false

3. 再次访问 double.value:
   - dirty === false,直接返回缓存 0(不重新计算)

4. 修改 state.count = 1:
   - trigger(state, 'count')
   - 触发 computed 的 effect.scheduler()
   - scheduler 中:dirty = true(只标记,不计算)
   - trigger(double, 'value') 通知依赖 double 的 effect

5. 再次访问 double.value:
   - dirty === true,重新计算
   - _value = 2, dirty = false

computed 的懒计算特性

const state = reactive({ a: 1, b: 2 })

const sum = computed(() => {
  console.log('计算 sum')
  return state.a + state.b
})

state.a = 10 // 不会输出"计算 sum"(未访问 computed)
state.b = 20 // 不会输出"计算 sum"

console.log(sum.value) // 输出:计算 sum 30(访问时才计算)

核心优势

  • ✅ 只在依赖变化 + 被访问时才重新计算

  • ✅ 避免不必要的计算开销

computed 的缓存机制

const state = reactive({ count: 0 })

const expensive = computed(() => {
  console.log('执行复杂计算...')
  let sum = 0
  for (let i = 0; i < 1000000; i++) {
    sum += state.count
  }
  return sum
})

// 在一次渲染中多次访问
effect(() => {
  console.log(expensive.value)
  console.log(expensive.value)
  console.log(expensive.value)
})

/**
 * 只会输出一次"执行复杂计算..."
 * 因为:
 * 1. 首次访问:dirty = true,执行计算,缓存结果
 * 2. 后续访问:dirty = false,直接返回缓存
 */

computed vs 普通函数

const state = reactive({ count: 0 })

// ❌ 普通函数:每次调用都重新计算
function getDouble() {
  console.log('计算...')
  return state.count * 2
}

// ✅ computed:有缓存,只在依赖变化时重新计算
const double = computed(() => {
  console.log('计算...')
  return state.count * 2
})

effect(() => {
  console.log(getDouble()) // 每次都输出"计算..."
  console.log(getDouble())
  console.log(getDouble())
  
  console.log(double.value) // 只输出一次"计算..."
  console.log(double.value)
  console.log(double.value)
})

computed 的链式依赖

const state = reactive({ count: 0 })

const double = computed(() => state.count * 2)
const quadruple = computed(() => double.value * 2)

effect(() => {
  console.log('quadruple:', quadruple.value)
})

/**
 * 依赖链:
 * state.count → double → quadruple → effect
 * 
 * state.count = 1 时:
 * 1. trigger(state, 'count')
 * 2. double.scheduler() → double.dirty = true, trigger(double, 'value')
 * 3. quadruple.scheduler() → quadruple.dirty = true, trigger(quadruple, 'value')
 * 4. effect 重新执行
 * 5. 访问 quadruple.value → 重新计算 → 访问 double.value → 重新计算
 */

watch 侦听器

watch 是 Vue3 中用于监听数据变化并执行自定义回调的 API,本质是对 effect 的封装,但与普通 effect、computed 相比,有明确的“监听目标”和“回调执行时机”,更适合处理“数据变化后做额外操作”的场景(如接口请求、日志打印、状态同步等)。

watch 核心特点:可监听单个/多个响应式数据,支持惰性执行/立即执行,可获取新旧值,支持深度监听,还能手动停止监听。

watch 的底层实现原理

watch 的本质是创建一个带有“监听逻辑”的 effect,核心流程分为 3 步:

  1. 创建 getter 函数:用于读取监听目标的值(单个值/多个值),触发依赖收集;

  2. 创建 effect:将 getter 作为 effect 的副作用函数,同时传入自定义调度器(scheduler);

  3. 依赖变化时:trigger 触发 effect 的调度器,在调度器中执行用户传入的回调,并传入新旧值。

简化版源码实现(贴合 Vue3 核心逻辑):

function watch(source, callback, options = {}) {
  // 1. 处理监听目标:将 source 转为 getter 函数(统一格式)
  let getter
  if (typeof source === 'function') {
    // 监听函数返回值(如 watch(() => state.count, ...))
    getter = source
  } else if (isReactive(source)) {
    // 监听整个响应式对象(深度监听)
    getter = () => traverse(source)
  } else if (isRef(source)) {
    // 监听 ref
    getter = () => source.value
  } else {
    // 非响应式数据,不收集依赖
    getter = () => {}
  }

  // 2. 存储旧值(首次执行时获取)
  let oldValue

  // 3. 创建调度器:依赖变化时执行回调
  const scheduler = () => {
    const newValue = runner() // 重新执行 getter,获取新值
    // 执行用户回调,传入新旧值
    callback(newValue, oldValue, () => {
      // 手动触发监听(forceUpdate)
      runner()
    })
    // 更新旧值,为下一次变化做准备
    oldValue = newValue
  }

  // 4. 创建 effect:getter 作为副作用,scheduler 控制回调执行
  const runner = effect(getter, {
    lazy: true, // 懒执行:首次不触发 effect(默认不执行回调)
    scheduler // 依赖变化时,执行调度器(而非直接执行 effect)
  })

  // 5. 处理立即执行(immediate: true)
  if (options.immediate) {
    scheduler() // 立即执行回调,此时 oldValue 为 undefined
  } else {
    oldValue = runner() // 首次执行 getter,获取旧值
  }

  // 6. 返回停止监听的函数
  return () => {
    runner.effect.stop() // 停止 effect,清理依赖
  }
}

// 辅助函数:遍历响应式对象,触发所有属性的 get,实现深度监听
function traverse(value, seen = new Set()) {
  if (!isObject(value) || seen.has(value)) return value
  seen.add(value)
  for (const key in value) {
    traverse(value[key], seen)
  }
  return value
}

watch 的核心使用场景与示例

Vue3 中 watch 有 3 种常见使用方式,覆盖不同监听场景,面试常考:

1. 监听单个响应式数据(最常用)

监听 reactive 对象的单个属性、ref 值,适合精准监听某个数据的变化。

const state = reactive({ count: 0, name: 'Vue' })
const countRef = ref(0)

// 监听 reactive 单个属性(需用函数返回)
watch(
  () => state.count,
  (newVal, oldVal) => {
    console.log('count 变化:', oldVal, '→', newVal)
  }
)

// 监听 ref 值(直接传入)
watch(
  countRef,
  (newVal, oldVal) => {
    console.log('countRef 变化:', oldVal, '→', newVal)
  }
)

// 触发变化
state.count++ // 输出:count 变化:0 → 1
countRef.value++ // 输出:countRef 变化:0 → 1

2. 监听多个响应式数据

传入数组,监听多个数据,任意一个变化都会触发回调,回调中接收的新旧值也是数组。

const state = reactive({ count: 0, name: 'Vue' })

watch(
  [() => state.count, () => state.name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log('count 变化:', oldCount, '→', newCount)
    console.log('name 变化:', oldName, '→', newName)
  }
)

// 触发变化
state.count++ // 输出 count 变化,name 无变化
state.name = 'Vue3' // 输出 name 变化,count 无变化

3. 监听整个 reactive 对象(深度监听)

直接传入 reactive 对象,默认会深度监听(无需手动设置 deep: true),任意嵌套属性变化都会触发回调。

const state = reactive({
  user: {
    profile: {
      age: 18
    }
  }
})

// 监听整个 reactive 对象
watch(
  state,
  (newVal, oldVal) => {
    console.log('state 变化:', oldVal.user.profile.age, '→', newVal.user.profile.age)
  }
)

// 触发深层变化
state.user.profile.age = 20 // 输出:state 变化:18 → 20

watch 的关键配置项(面试高频)

watch 支持 3 个核心配置项,决定回调的执行时机和行为,是面试重点:

1. immediate:立即执行回调

默认 false(惰性执行):只有数据变化时才执行回调;设置为 true,watch 初始化时会立即执行一次回调(此时 oldValueundefined)。

watch(
  () => state.count,
  (newVal, oldVal) => {
    console.log('回调执行:', oldVal, '→', newVal)
  },
  { immediate: true } // 立即执行
)
// 初始化时输出:回调执行:undefined → 0(无需修改 count)

2. deep:深度监听

默认情况:

  • 监听 reactive 对象:默认 deep: true,自动深度监听;

  • 监听单个属性(如 () => state.user.profile.age):默认 deep: false,只监听当前属性,不监听嵌套属性;

  • 手动设置 deep: true,可实现对单个属性的深度监听(极少用)。

const state = reactive({
  user: {
    profile: {
      age: 18
    }
  }
})

// 监听单个深层属性,需手动设置 deep: true
watch(
  () => state.user,
  (newVal) => {
    console.log('age 变化:', newVal.profile.age)
  },
  { deep: true }
)

state.user.profile.age = 20 // 输出:age 变化:20

3. flush:回调执行时机

控制回调在“DOM 更新前”“DOM 更新后”还是“同步执行”,默认flush: 'pre'(DOM 更新前执行),解决“回调中操作 DOM 但 DOM 未更新”的问题。

  • flush: 'pre'(默认):DOM 更新前执行回调;

  • flush: 'post':DOM 更新后执行回调(适合操作 DOM);

  • flush: 'sync':数据变化后立即同步执行回调(性能较差,慎用)。

watch(
  () => state.count,
  () => {
    console.log('DOM 状态:', document.getElementById('count').innerText)
  },
  { flush: 'post' } // DOM 更新后执行
)

state.count++ // 此时 DOM 已更新,能获取到最新的 innerText

watch vs computed(面试必问)

很多面试会问“watch 和 computed 有什么区别”,核心差异在于“用途”和“执行机制”,用表格清晰区分:

维度 watch computed
核心用途 监听数据变化,执行副作用(如接口请求、DOM 操作) 依赖数据计算,返回一个新值(用于模板渲染、逻辑复用)
执行机制 数据变化时触发回调(可配置立即执行) 懒计算 + 缓存,依赖变化且被访问时才重新计算
返回值 无返回值,靠回调执行逻辑 有返回值,可直接用于模板或逻辑
深度监听 需手动配置(监听单个属性时),默认支持 reactive 深度监听 自动深度监听依赖,无需配置
Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐