🔥 重学Vue之computed、watch、watchEffect原理与用途详解

🎯 学习目标:深入理解Vue3响应式系统中computed、watch、watchEffect的原理机制和实际应用场景

📊 难度等级 :中级-高级

🏷️ 技术标签#Vue3 #响应式系统 #computed #watch #watchEffect

⏱️ 阅读时间:约15分钟


🌟 引言

在Vue3的响应式系统中,你是否遇到过这样的困扰:

  • 性能问题:不知道什么时候用computed,什么时候用watch,导致不必要的重复计算
  • 监听困惑:watch和watchEffect看起来很相似,但不清楚具体区别和使用场景
  • 原理不明:只会用API但不理解底层原理,遇到复杂场景就束手无策
  • 最佳实践:缺乏实际项目中的最佳实践指导,代码质量参差不齐

今天分享Vue3响应式系统的3个核心API的深度解析,让你的Vue开发更加得心应手!


💡 核心技巧详解

1. computed:智能缓存的计算属性

🔍 应用场景

当你需要基于响应式数据进行复杂计算,且希望结果能够缓存以避免重复计算时使用。

❌ 常见问题

很多开发者习惯在模板中直接进行计算或使用methods

vue 复制代码
<template>
  <!-- ❌ 模板中直接计算,每次渲染都会执行 -->
  <div>{{ user.firstName + ' ' + user.lastName }}</div>
  <div>{{ getFullName() }}</div>
  
  <!-- ❌ 使用methods,每次访问都会重新计算 -->
  <div>{{ calculateTotal() }}</div>
</template>

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

const user = reactive({
  firstName: '张',
  lastName: '三'
})

// ❌ methods方式,没有缓存
const getFullName = () => {
  console.log('计算全名') // 每次都会执行
  return user.firstName + ' ' + user.lastName
}

const calculateTotal = () => {
  console.log('计算总价') // 每次都会执行
  return items.reduce((sum, item) => sum + item.price, 0)
}
</script>

✅ 推荐方案

使用computed实现智能缓存

vue 复制代码
<template>
  <!-- ✅ 使用computed,有缓存机制 -->
  <div>{{ fullName }}</div>
  <div>{{ totalPrice }}</div>
</template>

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

const user = reactive({
  firstName: '张',
  lastName: '三'
})

const items = reactive([
  { name: '商品1', price: 100 },
  { name: '商品2', price: 200 }
])

/**
 * 计算用户全名
 * @description 基于firstName和lastName计算完整姓名,具有缓存机制
 * @returns {string} 完整的用户姓名
 */
const fullName = computed(() => {
  console.log('计算全名') // 只有依赖变化时才执行
  return `${user.firstName} ${user.lastName}`
})

/**
 * 计算商品总价
 * @description 计算购物车中所有商品的总价格
 * @returns {number} 商品总价
 */
const totalPrice = computed(() => {
  console.log('计算总价') // 只有items变化时才执行
  return items.reduce((sum, item) => sum + item.price, 0)
})
</script>

🔍 computed底层实现原理

computed的核心特性是懒计算智能缓存,让我们深入了解其底层实现机制:

1. computed的完整实现结构
javascript 复制代码
/**
 * computed的完整底层实现
 * @param {Function} getterOrOptions - 计算函数或配置对象
 * @param {Object} debugOptions - 调试选项
 */
function computed(getterOrOptions, debugOptions) {
  let getter, setter
  
  // 处理参数:支持函数或对象形式
  if (typeof getterOrOptions === 'function') {
    getter = getterOrOptions
    setter = () => {
      console.warn('computed value is readonly')
    }
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  
  // 🔑 关键:创建computed实例
  const cRef = new ComputedRefImpl(getter, setter, debugOptions)
  return cRef
}

/**
 * ComputedRefImpl - computed的核心实现类
 */
class ComputedRefImpl {
  constructor(getter, setter, isReadonly = false) {
    this._getter = getter
    this._setter = setter
    this._value = undefined
    this._dirty = true  // 🔑 脏检查标记:控制是否需要重新计算
    this.effect = null  // 存储关联的effect
    this.__v_isRef = true
    this.__v_isReadonly = isReadonly
    
    // 🔑 关键:创建响应式effect,但设置为懒执行
    this.effect = new ReactiveEffect(getter, () => {
      // 🔑 调度器:依赖变化时的处理逻辑
      if (!this._dirty) {
        this._dirty = true
        // 触发computed本身的依赖更新
        triggerRefValue(this)
      }
    })
    
    this.effect.computed = this
  }
  
  // 🔑 关键:getter访问器 - 实现懒计算和缓存
  get value() {
    // 收集computed作为依赖
    trackRefValue(this)
    
    // 🔑 核心逻辑:只有在脏数据时才重新计算
    if (this._dirty) {
      this._dirty = false
      // 执行计算函数,收集内部依赖
      this._value = this.effect.run()
    }
    
    return this._value
  }
  
  // setter访问器
  set value(newValue) {
    this._setter(newValue)
  }
}
2. 懒计算机制的实现细节
javascript 复制代码
/**
 * ReactiveEffect - computed使用的effect实现
 */
class ReactiveEffect {
  constructor(fn, scheduler = null) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    this.deps = []  // 存储依赖的集合
  }
  
  // 🔑 关键:run方法 - 执行计算函数并收集依赖
  run() {
    if (!this.active) {
      return this.fn()
    }
    
    try {
      // 设置当前活跃的effect
      activeEffect = this
      
      // 🔑 关键:清理旧依赖,重新收集
      cleanupEffect(this)
      
      // 执行计算函数,期间会触发依赖收集
      return this.fn()
    } finally {
      // 重置活跃effect
      activeEffect = null
    }
  }
  
  // 停止effect
  stop() {
    if (this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}

/**
 * 清理effect的依赖关系
 */
function cleanupEffect(effect) {
  effect.deps.forEach(dep => {
    dep.delete(effect)
  })
  effect.deps.length = 0
}
3. 缓存机制的核心逻辑
javascript 复制代码
/**
 * 依赖收集:trackRefValue
 * @description 当访问computed.value时调用
 */
function trackRefValue(ref) {
  if (activeEffect) {
    // 🔑 关键:将computed作为依赖源进行追踪
    trackEffects(ref.dep || (ref.dep = new Set()))
  }
}

/**
 * 依赖触发:triggerRefValue  
 * @description 当computed的依赖发生变化时调用
 */
function triggerRefValue(ref) {
  if (ref.dep) {
    // 🔑 关键:触发所有依赖computed的effect
    triggerEffects(ref.dep)
  }
}

/**
 * 触发effects执行
 */
function triggerEffects(dep) {
  for (const effect of dep) {
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        // 🔑 关键:使用调度器控制执行时机
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  }
}
4. 完整的工作流程示例
javascript 复制代码
/**
 * 演示computed的完整工作流程
 */
const state = reactive({ count: 1, multiplier: 2 })

// 1. 创建computed
const doubleCount = computed(() => {
  console.log('🔄 计算函数执行')
  return state.count * state.multiplier
})

console.log('📝 computed创建完成,但计算函数尚未执行')

// 2. 第一次访问 - 触发计算和依赖收集
console.log('🔍 第一次访问:', doubleCount.value)
// 输出:🔄 计算函数执行
// 输出:🔍 第一次访问: 2

// 3. 第二次访问 - 使用缓存
console.log('🔍 第二次访问:', doubleCount.value)
// 输出:🔍 第二次访问: 2 (没有"计算函数执行",说明使用了缓存)

// 4. 修改依赖 - 标记为脏数据但不立即计算
console.log('📝 修改state.count = 3')
state.count = 3
console.log('📝 依赖已变化,computed被标记为dirty,但尚未重新计算')

// 5. 再次访问 - 重新计算
console.log('🔍 第三次访问:', doubleCount.value)
// 输出:🔄 计算函数执行
// 输出:🔍 第三次访问: 6
5. 性能优化的关键点
javascript 复制代码
/**
 * computed性能优化的核心机制
 */

// ✅ 优化点1:懒执行 - 只有被访问时才计算
const expensiveComputed = computed(() => {
  console.log('执行昂贵的计算...')
  // 这里的计算只有在被访问时才会执行
  return heavyCalculation()
})

// ✅ 优化点2:智能缓存 - 依赖不变时复用结果
const cachedResult = computed(() => {
  return state.list.filter(item => item.active).length
})

// 多次访问使用缓存
console.log(cachedResult.value) // 执行计算
console.log(cachedResult.value) // 使用缓存
console.log(cachedResult.value) // 使用缓存

// ✅ 优化点3:精确依赖追踪 - 只有真正的依赖变化才重新计算
const smartComputed = computed(() => {
  // 只依赖state.a,state.b的变化不会触发重新计算
  return state.a * 2
})

state.b = 100 // 不会触发smartComputed重新计算
state.a = 5   // 会触发smartComputed重新计算

🧠 computed工作机制总结

  1. 创建阶段:创建ComputedRefImpl实例,设置懒执行的ReactiveEffect
  2. 首次访问:执行计算函数,收集依赖,缓存结果,标记为clean
  3. 缓存访问:直接返回缓存值,不执行计算函数
  4. 依赖变化:调度器将computed标记为dirty,触发computed的依赖更新
  5. 重新计算:下次访问时检测到dirty,重新执行计算函数并更新缓存

💡 核心要点

  • 缓存机制:只有依赖的响应式数据发生变化时才重新计算
  • 惰性求值:只有被访问时才会执行计算函数
  • 依赖追踪:自动追踪计算函数中使用的响应式数据

🎯 实际应用

在电商项目中计算购物车信息

vue 复制代码
<script setup>
import { reactive, computed } from 'vue'

const cart = reactive({
  items: [
    { id: 1, name: 'iPhone', price: 6999, quantity: 1, selected: true },
    { id: 2, name: 'iPad', price: 3999, quantity: 2, selected: false },
    { id: 3, name: 'MacBook', price: 12999, quantity: 1, selected: true }
  ]
})

/**
 * 计算已选中的商品
 * @description 过滤出用户选中的商品列表
 * @returns {Array} 已选中的商品数组
 */
const selectedItems = computed(() => {
  return cart.items.filter(item => item.selected)
})

/**
 * 计算选中商品的总价
 * @description 计算用户选中商品的总金额
 * @returns {number} 选中商品总价
 */
const selectedTotal = computed(() => {
  return selectedItems.value.reduce((sum, item) => {
    return sum + (item.price * item.quantity)
  }, 0)
})

/**
 * 计算优惠后价格
 * @description 根据总价计算优惠后的最终价格
 * @returns {number} 优惠后价格
 */
const finalPrice = computed(() => {
  const total = selectedTotal.value
  if (total > 10000) return total * 0.9 // 9折
  if (total > 5000) return total * 0.95  // 95折
  return total
})
</script>

2. watch:精确监听的数据观察者

🔍 应用场景

当你需要在数据变化时执行副作用操作(如API调用、DOM操作等)时使用。

❌ 常见问题

不理解watch的监听机制和配置选项

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

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

// ❌ 直接监听reactive对象,无法获取旧值
watch(user, (newVal, oldVal) => {
  console.log(newVal, oldVal) // oldVal和newVal是同一个对象
})

// ❌ 监听深层属性但没有开启deep选项
watch(user.name, (newVal) => {
  console.log('用户名变化:', newVal) // 不会触发
})
</script>

✅ 推荐方案

正确使用watch的各种监听方式

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

const count = ref(0)
const user = reactive({ 
  name: '张三', 
  age: 25,
  profile: {
    email: 'zhangsan@example.com'
  }
})

/**
 * 监听ref值的变化
 * @description 监听count的变化并执行相应操作
 */
watch(count, (newVal, oldVal) => {
  console.log(`count从 ${oldVal} 变为 ${newVal}`)
  // 执行副作用操作
  updateCountInAPI(newVal)
})

/**
 * 监听reactive对象的特定属性
 * @description 使用getter函数监听user.name的变化
 */
watch(
  () => user.name,
  (newName, oldName) => {
    console.log(`用户名从 ${oldName} 变为 ${newName}`)
    // 更新用户信息到服务器
    updateUserName(newName)
  }
)

/**
 * 深度监听reactive对象
 * @description 监听user对象的所有属性变化
 */
watch(
  user,
  (newUser, oldUser) => {
    console.log('用户信息发生变化')
    // 保存用户信息
    saveUserProfile(newUser)
  },
  { deep: true }
)

/**
 * 监听多个数据源
 * @description 同时监听多个响应式数据的变化
 */
watch(
  [count, () => user.name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log('count或用户名发生变化')
    // 执行复合逻辑
    handleMultipleChanges(newCount, newName)
  }
)

/**
 * 立即执行的监听器
 * @description 组件挂载时立即执行一次
 */
watch(
  () => user.profile.email,
  (newEmail) => {
    console.log('邮箱变化:', newEmail)
    validateEmail(newEmail)
  },
  { immediate: true }
)

// 模拟API调用函数
const updateCountInAPI = (count) => {
  console.log(`API调用: 更新count为 ${count}`)
}

const updateUserName = (name) => {
  console.log(`API调用: 更新用户名为 ${name}`)
}

const saveUserProfile = (user) => {
  console.log('API调用: 保存用户信息', user)
}

const handleMultipleChanges = (count, name) => {
  console.log(`复合操作: count=${count}, name=${name}`)
}

const validateEmail = (email) => {
  console.log(`验证邮箱: ${email}`)
}
</script>

🔍 watch底层实现原理

watch的核心特性是精确监听回调执行,让我们深入了解其底层实现机制:

1. watch的完整实现结构
javascript 复制代码
/**
 * watch的完整底层实现
 * @param {Function|Object|Array} source - 监听源
 * @param {Function} cb - 回调函数
 * @param {Object} options - 配置选项
 */
function watch(source, cb, options = {}) {
  return doWatch(source, cb, options)
}

/**
 * doWatch - watch的核心实现函数
 */
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = {}) {
  let getter
  let forceTrigger = false
  let isMultiSource = false
  
  // 🔑 关键:处理不同类型的监听源
  if (isRef(source)) {
    // 监听ref
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    // 监听reactive对象
    getter = () => source
    deep = true // reactive对象默认深度监听
  } else if (Array.isArray(source)) {
    // 监听多个源
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () => source.map(s => {
      if (isRef(s)) return s.value
      if (isReactive(s)) return traverse(s)
      if (typeof s === 'function') return callWithErrorHandling(s)
      return s
    })
  } else if (typeof source === 'function') {
    // 监听函数返回值
    if (cb) {
      // watch(fn, callback)
      getter = () => callWithErrorHandling(source)
    } else {
      // watchEffect(fn)
      getter = () => {
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(source, [onCleanup])
      }
    }
  } else {
    getter = () => {}
  }
  
  // 🔑 关键:深度监听的实现
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  let cleanup
  let onCleanup = (fn) => {
    cleanup = effect.onStop = () => {
      callWithErrorHandling(fn)
    }
  }
  
  let oldValue = isMultiSource ? [] : {}
  
  // 🔑 关键:调度器 - 控制回调执行时机
  const job = () => {
    if (!effect.active) return
    
    if (cb) {
      // watch with callback
      const newValue = effect.run()
      
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? newValue.some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue))
      ) {
        // 清理副作用
        if (cleanup) {
          cleanup()
        }
        
        // 🔑 关键:执行回调函数
        callWithAsyncErrorHandling(cb, [
          newValue,
          oldValue === {} ? undefined : oldValue,
          onCleanup
        ])
        
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }
  
  // 设置调度器的执行时机
  let scheduler
  if (flush === 'sync') {
    scheduler = job // 同步执行
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job) // 组件更新后执行
  } else {
    // 默认 'pre' - 组件更新前执行
    scheduler = () => queuePreFlushCb(job)
  }
  
  // 🔑 关键:创建响应式effect
  const effect = new ReactiveEffect(getter, scheduler)
  
  // 初始执行
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = effect.run()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(effect.run.bind(effect))
  } else {
    effect.run()
  }
  
  // 返回停止监听的函数
  return () => {
    effect.stop()
  }
}
2. 深度遍历机制的实现
javascript 复制代码
/**
 * traverse - 深度遍历函数,用于深度监听
 * @param {*} value - 要遍历的值
 * @param {Set} seen - 已访问的对象集合,防止循环引用
 */
function traverse(value, seen) {
  if (!isObject(value) || value[ReactiveFlags.SKIP]) {
    return value
  }
  
  seen = seen || new Set()
  
  // 🔑 关键:防止循环引用
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  
  // 🔑 关键:根据不同类型进行遍历
  if (isRef(value)) {
    // 遍历ref的value
    traverse(value.value, seen)
  } else if (Array.isArray(value)) {
    // 遍历数组的每个元素
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    // 遍历Set和Map
    value.forEach((v) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    // 🔑 关键:遍历对象的所有属性,触发getter收集依赖
    for (const key in value) {
      traverse(value[key], seen)
    }
  }
  
  return value
}
3. 监听源处理的具体实现
javascript 复制代码
/**
 * 不同监听源的处理策略
 */

// 1. 监听ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`)
})
// 实际监听的是 () => count.value

// 2. 监听reactive对象的特定属性
const state = reactive({ user: { name: 'Vue' } })
watch(() => state.user.name, (newVal, oldVal) => {
  console.log(`name: ${oldVal} -> ${newVal}`)
})
// getter函数会在执行时收集state.user.name的依赖

// 3. 监听整个reactive对象(深度监听)
watch(state, (newVal, oldVal) => {
  console.log('state changed')
}, { deep: true })
// 会调用traverse(state)遍历所有属性

// 4. 监听多个源
watch([count, () => state.user.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`count: ${oldCount} -> ${newCount}`)
  console.log(`name: ${oldName} -> ${newName}`)
})
4. 调度器和执行时机控制
javascript 复制代码
/**
 * watch的调度器实现 - 控制回调执行时机
 */

// 🔑 关键:不同的flush选项控制执行时机
const flushJobs = {
  // 同步执行 - 立即执行
  sync: (job) => job(),
  
  // 组件更新前执行 - 默认行为
  pre: (job) => {
    queuePreFlushCb(job)
  },
  
  // 组件更新后执行
  post: (job) => {
    queuePostRenderEffect(job)
  }
}

/**
 * 队列管理 - 批量处理回调
 */
const queue = []
let isFlushing = false
let isFlushPending = false

function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
  }
  queueFlush()
}

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

function flushJobs() {
  isFlushPending = false
  isFlushing = true
  
  // 🔑 关键:按顺序执行所有job
  queue.sort((a, b) => getId(a) - getId(b))
  
  for (let i = 0; i < queue.length; i++) {
    const job = queue[i]
    if (job && job.active !== false) {
      callWithErrorHandling(job)
    }
  }
  
  queue.length = 0
  isFlushing = false
}
5. 完整的工作流程示例
javascript 复制代码
/**
 * 演示watch的完整工作流程
 */
const state = reactive({
  count: 0,
  user: {
    name: 'Vue',
    profile: {
      age: 3
    }
  }
})

// 1. 创建watch - 设置监听和回调
const stopWatcher = watch(
  () => state.user.name, // getter函数
  (newName, oldName) => {
    console.log(`🔄 用户名从 ${oldName} 变为 ${newName}`)
  },
  { immediate: true } // 立即执行一次
)

console.log('📝 watch创建完成')
// 输出:🔄 用户名从 undefined 变为 Vue (immediate执行)

// 2. 修改监听的属性 - 触发回调
console.log('📝 修改用户名')
state.user.name = 'React'
// 输出:🔄 用户名从 Vue 变为 React

// 3. 修改非监听的属性 - 不触发回调
console.log('📝 修改年龄(不会触发回调)')
state.user.profile.age = 4
// 无输出,因为没有监听age属性

// 4. 停止监听
console.log('📝 停止监听')
stopWatcher()

// 5. 再次修改 - 不会触发回调
state.user.name = 'Angular'
// 无输出,因为已停止监听
6. 性能优化策略
javascript 复制代码
/**
 * watch性能优化的关键技巧
 */

// ✅ 优化点1:精确监听 - 只监听需要的属性
watch(() => state.user.name, callback) // 只监听name
// 而不是
watch(state.user, callback, { deep: true }) // 监听整个user对象

// ✅ 优化点2:使用computed优化复杂计算
const expensiveValue = computed(() => {
  return state.list.filter(item => item.active).length
})
watch(expensiveValue, callback) // 监听computed而不是原始数据

// ✅ 优化点3:合理使用flush选项
watch(source, callback, { flush: 'post' }) // 在DOM更新后执行

// ✅ 优化点4:及时清理监听器
const stop = watch(source, callback)
onUnmounted(() => {
  stop() // 组件卸载时停止监听
})

🧠 watch工作机制总结

  1. 创建阶段:解析监听源,创建getter函数和调度器
  2. 依赖收集:执行getter函数,收集相关依赖
  3. 监听等待:等待依赖变化,不执行任何操作
  4. 变化检测:依赖变化时,调度器重新执行getter获取新值
  5. 回调执行:比较新旧值,如有变化则执行回调函数
  6. 清理机制:支持手动停止监听和自动清理

💡 核心要点

  • 精确监听:可以监听特定的数据源,不会产生不必要的触发
  • 获取新旧值:能够获取变化前后的值,便于对比处理
  • 配置选项:支持deep、immediate等配置选项

🎯 实际应用

在搜索功能中实现防抖和API调用

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

const searchKeyword = ref('')
const searchResults = ref([])
const loading = ref(false)

/**
 * 搜索API调用函数
 * @description 调用搜索接口获取结果
 * @param {string} keyword - 搜索关键词
 * @returns {Promise<Array>} 搜索结果
 */
const searchAPI = async (keyword) => {
  const response = await fetch(`/api/search?q=${keyword}`)
  return response.json()
}

/**
 * 防抖搜索监听器
 * @description 监听搜索关键词变化,实现防抖搜索
 */
let searchTimer = null
watch(
  searchKeyword,
  async (newKeyword, oldKeyword) => {
    // 清除之前的定时器
    if (searchTimer) {
      clearTimeout(searchTimer)
    }
    
    // 如果关键词为空,清空结果
    if (!newKeyword.trim()) {
      searchResults.value = []
      return
    }
    
    // 设置防抖延迟
    searchTimer = setTimeout(async () => {
      try {
        loading.value = true
        console.log(`搜索关键词从 "${oldKeyword}" 变为 "${newKeyword}"`)
        
        const results = await searchAPI(newKeyword)
        searchResults.value = results
      } catch (error) {
        console.error('搜索失败:', error)
        searchResults.value = []
      } finally {
        loading.value = false
      }
    }, 300) // 300ms防抖延迟
  }
)
</script>

3. watchEffect:自动依赖追踪的副作用函数

🔍 应用场景

当你需要执行副作用操作,且希望自动追踪依赖而不需要明确指定监听目标时使用。

❌ 常见问题

不理解watchEffect的自动依赖追踪机制

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

const count = ref(0)
const name = ref('张三')

// ❌ 在watchEffect中使用了响应式数据但没有正确理解依赖追踪
watchEffect(() => {
  console.log('副作用执行')
  // 只使用了count,但开发者可能认为name变化也会触发
  if (count.value > 0) {
    console.log(`count: ${count.value}`)
  }
  // name没有被使用,所以name变化不会触发这个watchEffect
})
</script>

✅ 推荐方案

正确使用watchEffect的自动依赖追踪

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

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

/**
 * 自动依赖追踪的副作用函数
 * @description 自动追踪函数中使用的所有响应式数据
 */
watchEffect(() => {
  console.log('watchEffect执行')
  
  // 这里使用了count和user.name,所以这两个数据变化都会触发
  if (count.value > 0) {
    console.log(`用户 ${user.name} 的计数: ${count.value}`)
  }
  
  // 条件性依赖:只有当isVisible为true时,才会追踪user.age
  if (isVisible.value) {
    console.log(`用户年龄: ${user.age}`)
  }
})

/**
 * 清理副作用的watchEffect
 * @description 演示如何在watchEffect中进行清理操作
 */
watchEffect((onInvalidate) => {
  const timer = setInterval(() => {
    console.log(`定时器执行,当前count: ${count.value}`)
  }, 1000)
  
  // 注册清理函数
  onInvalidate(() => {
    console.log('清理定时器')
    clearInterval(timer)
  })
})

/**
 * 停止监听的示例
 * @description 演示如何手动停止watchEffect
 */
const stopWatcher = watchEffect(() => {
  console.log(`监听用户名变化: ${user.name}`)
})

// 在某个条件下停止监听
const handleStopWatching = () => {
  stopWatcher()
  console.log('已停止监听')
}
</script>

🔍 watchEffect底层实现原理

watchEffect的核心特性是自动依赖收集立即执行,让我们深入了解其底层实现机制:

1. watchEffect的完整实现结构
javascript 复制代码
/**
 * watchEffect的完整底层实现
 * @param {Function} effect - 副作用函数
 * @param {Object} options - 配置选项
 */
function watchEffect(effect, options) {
  return doWatch(effect, null, options)
}

/**
 * watchPostEffect - 在组件更新后执行的watchEffect
 */
function watchPostEffect(effect, options) {
  return doWatch(
    effect,
    null,
    { ...options, flush: 'post' }
  )
}

/**
 * watchSyncEffect - 同步执行的watchEffect
 */
function watchSyncEffect(effect, options) {
  return doWatch(
    effect,
    null,
    { ...options, flush: 'sync' }
  )
}

/**
 * doWatch中watchEffect的特殊处理逻辑
 */
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = {}) {
  let getter
  let cleanup
  
  // 🔑 关键:watchEffect的特殊处理(cb为null)
  if (!cb) {
    // watchEffect模式
    let onCleanup = (fn) => {
      cleanup = effect.onStop = () => {
        callWithErrorHandling(fn)
      }
    }
    
    // 🔑 关键:包装effect函数,注入清理机制
    getter = () => {
      if (cleanup) {
        cleanup()
      }
      return callWithAsyncErrorHandling(source, [onCleanup])
    }
  }
  
  // 🔑 关键:调度器 - 控制执行时机
  const job = () => {
    if (!effect.active) return
    
    if (!cb) {
      // watchEffect模式 - 直接执行
      effect.run()
    }
  }
  
  // 设置调度器
  let scheduler
  if (flush === 'sync') {
    scheduler = job // 同步执行
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job) // 组件更新后执行
  } else {
    // 默认 'pre' - 组件更新前执行
    scheduler = () => queuePreFlushCb(job)
  }
  
  // 🔑 关键:创建响应式effect
  const effect = new ReactiveEffect(getter, scheduler)
  
  // 🔑 关键:立即执行(watchEffect的核心特性)
  if (flush === 'post') {
    queuePostRenderEffect(effect.run.bind(effect))
  } else {
    effect.run() // 立即执行,自动收集依赖
  }
  
  // 返回停止函数
  return () => {
    effect.stop()
  }
}
2. 自动依赖收集机制的实现
javascript 复制代码
/**
 * ReactiveEffect - 响应式effect的核心实现
 */
class ReactiveEffect {
  constructor(fn, scheduler = null, scope) {
    this.fn = fn
    this.scheduler = scheduler
    this.active = true
    this.deps = [] // 🔑 关键:存储依赖关系
    this.parent = undefined
    
    recordEffectScope(this, scope)
  }
  
  // 🔑 关键:执行effect并收集依赖
  run() {
    if (!this.active) {
      return this.fn()
    }
    
    let parent = activeEffect
    let lastShouldTrack = shouldTrack
    
    try {
      // 🔑 关键:设置当前活跃的effect
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
      
      // 🔑 关键:清理旧的依赖关系
      cleanupEffect(this)
      
      // 🔑 关键:执行函数,期间访问的响应式数据会自动收集依赖
      return this.fn()
    } finally {
      // 恢复之前的状态
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
    }
  }
  
  // 停止effect
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

/**
 * 清理effect的依赖关系
 */
function cleanupEffect(effect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect) // 从依赖集合中移除当前effect
    }
    deps.length = 0
  }
}
3. 依赖收集和触发机制
javascript 复制代码
/**
 * track - 依赖收集函数
 * 在响应式对象的getter中调用
 */
function track(target, type, key) {
  if (shouldTrack && activeEffect) {
    // 🔑 关键:获取目标对象的依赖映射
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    
    // 🔑 关键:获取特定属性的依赖集合
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }
    
    // 🔑 关键:建立双向依赖关系
    trackEffects(dep)
  }
}

/**
 * trackEffects - 建立effect和依赖的双向关系
 */
function trackEffects(dep) {
  let shouldTrack = false
  
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // 标记为新依赖
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // 降级处理
    shouldTrack = !dep.has(activeEffect)
  }
  
  if (shouldTrack) {
    // 🔑 关键:双向绑定
    dep.add(activeEffect) // 依赖集合中添加当前effect
    activeEffect.deps.push(dep) // effect中记录依赖
  }
}

/**
 * trigger - 依赖触发函数
 * 在响应式对象的setter中调用
 */
function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 没有依赖,直接返回
    return
  }
  
  let deps = []
  
  // 🔑 关键:收集需要触发的effect
  if (type === TriggerOpTypes.CLEAR) {
    // 清空操作,触发所有依赖
    deps = [...depsMap.values()]
  } else if (key === 'length' && Array.isArray(target)) {
    // 数组长度变化的特殊处理
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newValue) {
        deps.push(dep)
      }
    })
  } else {
    // 普通属性变化
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    
    // 处理添加/删除操作
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!Array.isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        } else if (isIntegerKey(key)) {
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!Array.isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
  
  // 🔑 关键:触发所有相关的effect
  if (deps.length === 1) {
    if (deps[0]) {
      triggerEffects(deps[0])
    }
  } else {
    const effects = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    triggerEffects(createDep(effects))
  }
}
4. 清理机制的完整实现
javascript 复制代码
/**
 * watchEffect的清理机制实现
 */

// 示例:完整的清理机制演示
const state = reactive({ count: 0 })

const stop = watchEffect((onCleanup) => {
  console.log('🔄 watchEffect执行,count:', state.count)
  
  // 🔑 关键:创建副作用(定时器、事件监听器、网络请求等)
  const timer = setInterval(() => {
    console.log('⏰ 定时器tick')
  }, 1000)
  
  const controller = new AbortController()
  
  fetch('/api/data', { signal: controller.signal })
    .then(response => response.json())
    .then(data => {
      console.log('📡 数据获取成功:', data)
    })
    .catch(error => {
      if (error.name !== 'AbortError') {
        console.error('❌ 请求失败:', error)
      }
    })
  
  // 🔑 关键:注册清理函数
  onCleanup(() => {
    console.log('🧹 执行清理操作')
    clearInterval(timer) // 清理定时器
    controller.abort() // 取消网络请求
  })
})

// 触发重新执行 - 会先执行清理函数
setTimeout(() => {
  state.count++ // 触发watchEffect重新执行
}, 2000)

// 停止监听 - 会执行最后一次清理
setTimeout(() => {
  stop() // 停止watchEffect
}, 5000)
5. 动态依赖收集的实现原理
javascript 复制代码
/**
 * 演示watchEffect的动态依赖收集机制
 */
const state = reactive({
  mode: 'light',
  theme: {
    light: { bg: 'white', text: 'black' },
    dark: { bg: 'black', text: 'white' }
  },
  user: {
    name: 'Vue',
    avatar: 'vue.png'
  },
  showUser: true
})

// 🔑 关键:依赖会根据执行路径动态变化
const stop = watchEffect(() => {
  console.log('🎨 当前模式:', state.mode)
  
  // 🔑 关键:条件依赖收集
  if (state.mode === 'light') {
    console.log('☀️ 浅色主题:', state.theme.light.bg)
    // 只有在light模式下才会收集theme.light的依赖
  } else {
    console.log('🌙 深色主题:', state.theme.dark.bg)
    // 只有在dark模式下才会收集theme.dark的依赖
  }
  
  // 🔑 关键:动态依赖
  if (state.showUser) {
    console.log('👤 用户信息:', state.user.name)
    // 只有当showUser为true时才收集user.name的依赖
  }
})

// 测试动态依赖收集
console.log('📝 修改浅色主题背景色')
state.theme.light.bg = 'gray' // 触发(当前是light模式)

console.log('📝 修改深色主题背景色')
state.theme.dark.bg = 'navy' // 不触发(当前不是dark模式)

console.log('📝 切换到深色模式')
state.mode = 'dark' // 触发,且之后深色主题的变化会触发

console.log('📝 再次修改深色主题背景色')
state.theme.dark.bg = 'purple' // 现在会触发(已切换到dark模式)

console.log('📝 隐藏用户信息')
state.showUser = false // 触发,且之后用户信息变化不会触发

console.log('📝 修改用户名')
state.user.name = 'React' // 不触发(showUser为false)
6. 执行时机控制的实现
javascript 复制代码
/**
 * watchEffect的执行时机控制机制
 */

// 🔑 关键:不同flush选项的实现
const state = reactive({ count: 0 })

// 1. 默认 'pre' - 组件更新前执行
watchEffect(() => {
  console.log('⚡ pre flush:', state.count)
  // 在DOM更新前执行,适合数据预处理
})

// 2. 'post' - 组件更新后执行
watchPostEffect(() => {
  console.log('🔄 post flush:', state.count)
  // 在DOM更新后执行,适合DOM操作
  document.title = `Count: ${state.count}`
})

// 3. 'sync' - 同步执行
watchSyncEffect(() => {
  console.log('🚀 sync flush:', state.count)
  // 同步执行,谨慎使用,可能影响性能
})

/**
 * 队列调度的实现原理
 */
const queue = []
let isFlushing = false
let isFlushPending = false

// pre flush队列
const pendingPreFlushCbs = []
let activePreFlushCbs = null
let preFlushIndex = 0

// post render队列
const pendingPostFlushCbs = []
let activePostFlushCbs = null
let postFlushIndex = 0

function queuePreFlushCb(cb) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

function queuePostFlushCb(cb) {
  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

function queueCb(cb, activeQueue, pendingQueue, index) {
  if (!Array.isArray(cb)) {
    if (
      !activeQueue ||
      !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
    ) {
      pendingQueue.push(cb)
    }
  } else {
    pendingQueue.push(...cb)
  }
  queueFlush()
}

🧠 watchEffect工作机制总结

  1. 创建阶段:创建ReactiveEffect实例,设置调度器
  2. 立即执行:首次运行effect函数,自动收集依赖
  3. 依赖收集:执行过程中访问的响应式数据自动建立依赖关系
  4. 监听等待:等待依赖变化,不执行任何操作
  5. 重新执行:依赖变化时,先执行清理函数,再重新执行effect
  6. 动态更新:每次执行都会重新收集依赖,实现动态依赖追踪

💡 核心要点

  • 自动依赖追踪:自动追踪函数中使用的所有响应式数据
  • 立即执行:创建时立即执行一次
  • 清理机制:支持清理副作用,避免内存泄漏

🎯 实际应用

在数据同步和日志记录中的应用

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

const userSettings = reactive({
  theme: 'light',
  language: 'zh-CN',
  notifications: true
})

const currentPage = ref('home')
const userActivity = ref([])

/**
 * 自动保存用户设置
 * @description 当用户设置发生变化时自动保存到localStorage
 */
watchEffect(() => {
  const settings = {
    theme: userSettings.theme,
    language: userSettings.language,
    notifications: userSettings.notifications
  }
  
  console.log('保存用户设置:', settings)
  localStorage.setItem('userSettings', JSON.stringify(settings))
})

/**
 * 页面访问日志记录
 * @description 记录用户的页面访问行为
 */
watchEffect(() => {
  const logEntry = {
    page: currentPage.value,
    timestamp: new Date().toISOString(),
    theme: userSettings.theme
  }
  
  console.log('记录页面访问:', logEntry)
  
  // 添加到活动记录
  userActivity.value.push(logEntry)
  
  // 发送到分析服务
  sendAnalytics(logEntry)
})

/**
 * WebSocket连接管理
 * @description 根据用户设置和页面状态管理WebSocket连接
 */
watchEffect((onInvalidate) => {
  // 只有在特定页面且开启通知时才建立连接
  if (currentPage.value === 'dashboard' && userSettings.notifications) {
    console.log('建立WebSocket连接')
    
    const ws = new WebSocket('ws://localhost:8080')
    
    ws.onopen = () => {
      console.log('WebSocket连接已建立')
    }
    
    ws.onmessage = (event) => {
      console.log('收到消息:', event.data)
    }
    
    ws.onerror = (error) => {
      console.error('WebSocket错误:', error)
    }
    
    // 清理函数:关闭WebSocket连接
    onInvalidate(() => {
      console.log('关闭WebSocket连接')
      ws.close()
    })
  }
})

// 模拟分析服务调用
const sendAnalytics = (data) => {
  console.log('发送分析数据:', data)
  // 实际项目中这里会调用分析服务API
}
</script>

📊 技巧对比总结

API 使用场景 优势 注意事项
computed 基于响应式数据的计算 缓存机制、惰性求值 不能有副作用操作
watch 数据变化时的副作用操作 精确监听、获取新旧值 需要明确指定监听目标
watchEffect 自动依赖追踪的副作用 自动依赖追踪、立即执行 依赖关系可能不够明确

🎯 实战应用建议

最佳实践

  1. computed应用:用于模板中的计算属性、数据转换、过滤排序等纯计算场景
  2. watch应用:用于API调用、表单验证、路由监听等需要副作用的场景
  3. watchEffect应用:用于日志记录、数据同步、自动保存等自动化副作用场景

性能考虑

  • computed缓存:充分利用computed的缓存机制,避免重复计算
  • watch防抖:对于频繁变化的数据,使用防抖技术优化性能
  • watchEffect清理:及时清理副作用,避免内存泄漏和重复执行

选择指南

javascript 复制代码
// 选择computed的情况
const fullName = computed(() => firstName.value + ' ' + lastName.value)

// 选择watch的情况
watch(searchKeyword, async (newKeyword) => {
  const results = await searchAPI(newKeyword)
  searchResults.value = results
})

// 选择watchEffect的情况
watchEffect(() => {
  localStorage.setItem('settings', JSON.stringify(settings))
})

💡 总结

这3个Vue3响应式API在日常开发中各有所长,掌握它们能让你的Vue开发更加高效:

  1. computed核心:智能缓存的计算属性,适用于纯计算场景
  2. watch核心:精确监听的数据观察者,适用于副作用操作
  3. watchEffect核心:自动依赖追踪的副作用函数,适用于自动化场景

希望这些技巧能帮助你在Vue3开发中更好地处理响应式数据,写出更高效的代码!


🔗 相关资源


💡 今日收获:掌握了Vue3响应式系统的3个核心API,这些知识点在实际开发中非常实用。

如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有任何问题也欢迎在评论区讨论。 🚀

相关推荐
Amos_Web4 小时前
Rust实战课程--网络资源监控器(初版)
前端·后端·rust
神秘的猪头4 小时前
html5与js今日笔记
前端
Zyx20074 小时前
JavaScript 中的对象字面量与代理模式:用代码演绎“爱情故事”
javascript
程序猿小蒜4 小时前
基于springboot的基于智能推荐的卫生健康系统开发与设计
java·javascript·spring boot·后端·spring
渣哥4 小时前
IOC 容器的进化:ApplicationContext 在 Spring 中的核心地位
javascript·后端·面试
Zyx20074 小时前
🎹用 HTML5 打造“敲击乐”钢琴:前端三剑客的第一次交响曲
前端
小时前端4 小时前
面试官:我为什么总在浏览器存储问题上追问IndexedDB?
前端·浏览器
前端小菜哇4 小时前
前端如何优雅的写一个记忆化函数?
前端
今禾4 小时前
Git完全指南(下篇):Git高级技巧与问题解决
前端·git·github