computed()、watch() 与 watchEffect()

下面,我们来系统的梳理关于 computed、watch 与 watchEffect 的基本知识点:


一、核心概念与响应式基础

1.1 响应式依赖关系

Vue 的响应式系统基于 依赖收集触发更新 的机制:

响应式数据
依赖收集
创建依赖关系
数据变更
触发更新
执行副作用

1.2 三大 API 对比

特性 computed watch watchEffect
返回值 Ref 对象 停止函数 停止函数
依赖收集 自动 手动指定 自动
执行时机 惰性求值 响应变化 立即执行
主要用途 派生状态 响应变化执行操作 自动追踪副作用
新旧值 提供新旧值
异步支持 同步 支持异步 支持异步
首次执行 访问时执行 可配置 总是执行

二、computed 深度解析

2.1 基本使用与类型

import { ref, computed } from 'vue'

// 只读计算属性
const count = ref(0)
const double = computed(() => count.value * 2)

// 可写计算属性
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newValue) => {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

2.2 实现原理

用户代码 computed() ReactiveEffect Dep 数据变更 用户再次访问 创建计算属性 创建 ReactiveEffect 收集依赖 访问 .value 执行计算函数 追踪依赖 返回计算结果 触发更新 标记为脏值 重新计算 用户代码 computed() ReactiveEffect Dep 数据变更 用户再次访问

2.3 核心特性

  1. 惰性求值:仅在访问 .value 时计算
  2. 结果缓存:依赖未变化时返回缓存值
  3. 依赖追踪:自动收集响应式依赖
  4. 类型安全:完美支持 TypeScript 类型推断

2.4 最佳实践

// 避免在计算属性中产生副作用
const badExample = computed(() => {
  console.log('This is a side effect!') // 避免
  return count.value * 2
})

// 复杂计算使用计算属性
const totalPrice = computed(() => {
  return cartItems.value.reduce((total, item) => {
    return total + (item.price * item.quantity)
  }, 0)
})

// 组合多个计算属性
const discountedTotal = computed(() => {
  return totalPrice.value * (1 - discountRate.value)
})

三、watch 深度解析

3.1 基本使用与语法

import { watch, ref } from 'vue'

// 侦听单个源
const count = ref(0)
watch(count, (newValue, oldValue) => {
  console.log(`Count changed: ${oldValue}${newValue}`)
})

// 侦听多个源
const firstName = ref('John')
const lastName = ref('Doe')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Name changed: ${oldFirst} ${oldLast}${newFirst} ${newLast}`)
})

// 深度侦听对象
const state = reactive({ user: { name: 'Alice' } })
watch(
  () => state.user,
  (newUser, oldUser) => {
    console.log('User changed', newUser, oldUser)
  },
  { deep: true }
)

3.2 配置选项详解

watch(source, callback, {
  // 立即执行回调
  immediate: true,
  
  // 深度侦听
  deep: true,
  
  // 回调执行时机
  flush: 'post', // 'pre' | 'post' | 'sync'
  
  // 调试钩子
  onTrack: (event) => debugger,
  onTrigger: (event) => debugger
})

3.3 高级用法

// 异步操作与取消
const data = ref(null)
watch(id, async (newId, oldId, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  
  try {
    const response = await fetch(`/api/data/${newId}`, {
      signal: controller.signal
    })
    data.value = await response.json()
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Fetch error:', error)
    }
  }
})

// 限制执行频率
import { throttle } from 'lodash-es'
watch(
  searchQuery,
  throttle((newQuery) => {
    search(newQuery)
  }, 500)
)

3.4 性能优化

// 避免深度侦听大型对象
watch(
  () => state.items.length, // 仅侦听长度变化
  (newLength) => {
    console.log('Item count changed:', newLength)
  }
)

// 使用浅层侦听
watch(
  () => ({ ...shallowObject }), // 创建浅拷贝
  (newObj) => {
    console.log('Shallow object changed')
  }
)

四、watchEffect 深度解析

4.1 基本使用

import { watchEffect, ref } from 'vue'

const count = ref(0)

// 自动追踪依赖
const stop = watchEffect((onCleanup) => {
  console.log(`Count: ${count.value}`)
  
  // 清理副作用
  onCleanup(() => {
    console.log('Cleanup previous effect')
  })
})

// 停止侦听
stop()

4.2 核心特性

  1. 自动依赖收集:无需指定侦听源
  2. 立即执行:创建时立即运行一次
  3. 清理机制:提供 onCleanup 回调
  4. 异步支持:天然支持异步操作

4.3 高级用法

// DOM 操作副作用
const elementRef = ref(null)
watchEffect(() => {
  if (elementRef.value) {
    // 操作 DOM
    elementRef.value.focus()
  }
})

// 响应式日志
watchEffect(() => {
  console.log('State updated:', {
    count: count.value,
    user: user.value.name
  })
})

// 组合多个副作用
watchEffect(async () => {
  const data = await fetchData(params.value)
  processData(data)
})

4.4 性能优化

// 使用 flush 控制执行时机
watchEffect(
  () => {
    // 在 DOM 更新后执行
    updateChart()
  },
  { flush: 'post' }
)

// 调试依赖
watchEffect(
  () => {
    // 副作用代码
  },
  {
    onTrack(e) {
      debugger // 依赖被追踪时
    },
    onTrigger(e) {
      debugger // 依赖变更触发回调时
    }
  }
)

五、三者的区别与选择指南

5.1 使用场景对比

场景 推荐 API 理由
派生状态 computed 自动缓存,高效计算
数据变化响应 watch 精确控制,获取新旧值
副作用管理 watchEffect 自动依赖收集
异步操作 watch/watchEffect 支持异步和取消
DOM 操作 watchEffect 自动追踪 DOM 依赖
调试依赖 watch 精确控制侦听源

5.2 性能考虑

  1. computed:适合同步计算,避免复杂操作
  2. watch:适合需要精确控制的场景
  3. watchEffect:适合自动依赖收集的副作用
// 计算属性 vs 侦听器
// 推荐:使用计算属性派生状态
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// 不推荐:使用侦听器模拟计算属性
const fullName = ref('')
watch([firstName, lastName], () => {
  fullName.value = `${firstName.value} ${lastName.value}`
})

5.3 组合使用模式

// 组合 computed 和 watch
const discountedTotal = computed(() => total.value * (1 - discount.value))

watch(discountedTotal, (newTotal) => {
  updateUI(newTotal)
})

// 组合 watchEffect 和 computed
const searchResults = ref([])
const searchQuery = ref('')

const validQuery = computed(() => 
  searchQuery.value.trim().length > 2
)

watchEffect(async (onCleanup) => {
  if (!validQuery.value) return
  
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  
  searchResults.value = await fetchSearchResults(
    searchQuery.value, 
    controller.signal
  )
})

六、原理深入剖析

6.1 Vue 响应式系统核心

订阅
1
*
使用
Dep
+depend()
+notify()
ReactiveEffect
+run()
+stop()
ComputedRefImpl
-_value
-_dirty
+get value()
+set value()

6.2 computed 实现原理

class ComputedRefImpl {
  constructor(getter, setter) {
    this._getter = getter
    this._setter = setter
    this._value = undefined
    this._dirty = true
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        trigger(this, 'value')
      }
    })
  }
  
  get value() {
    if (this._dirty) {
      this._value = this.effect.run()
      this._dirty = false
      track(this, 'value')
    }
    return this._value
  }
  
  set value(newValue) {
    this._setter(newValue)
  }
}

6.3 watch 和 watchEffect 的异同

实现机制 watch watchEffect
依赖收集 基于指定源 自动收集执行中的依赖
内部实现 基于 watchEffect 基础 API
调度机制 支持 flush 配置 支持 flush 配置
清理机制 通过回调参数 通过 onCleanup

七、最佳实践与性能优化

7.1 性能优化策略

  1. 避免不必要的重新计算

    // 使用 computed 缓存结果
    const filteredList = computed(() => 
      largeList.value.filter(item => item.active)
    )
    
  2. 合理使用侦听选项

    // 减少深度侦听范围
    watch(
      () => state.user.id, // 仅侦听 ID 变化
      (newId) => fetchUser(newId)
    )
    
  3. 批量更新处理

    watch(
      [data1, data2],
      () => {
        // 合并处理多个变化
        updateVisualization()
      },
      { flush: 'post' }
    )
    

7.2 常见模式

数据获取模式

const data = ref(null)
const error = ref(null)

watchEffect(async (onCleanup) => {
  data.value = null
  error.value = null
  
  const controller = new AbortController()
  onCleanup(() => controller.abort())
  
  try {
    const response = await fetch(url.value, {
      signal: controller.signal
    })
    data.value = await response.json()
  } catch (err) {
    if (err.name !== 'AbortError') {
      error.value = err.message
    }
  }
})

表单验证模式

const formState = reactive({ email: '', password: '' })
const errors = reactive({ email: '', password: '' })

watch(
  () => [formState.email, formState.password],
  () => {
    errors.email = formState.email.includes('@') ? '' : 'Invalid email'
    errors.password = formState.password.length >= 6 ? '' : 'Too short'
  },
  { immediate: true }
)

八、常见问题与解决方案

8.1 响应式依赖问题

问题: watchEffect 未正确追踪依赖

const state = reactive({ count: 0 })

watchEffect(() => {
  // 当 state.count 变化时不会触发
  console.log(state.nested?.value) // 可选链导致依赖丢失
})

解决方案:

watchEffect(() => {
  // 显式访问确保依赖追踪
  if (state.nested) {
    console.log(state.nested.value)
  }
})

8.2 异步竞态问题

问题: 多个异步请求可能导致旧数据覆盖新数据

watch(id, async (newId) => {
  const data = await fetchData(newId)
  currentData.value = data // 可能旧请求覆盖新
})

解决方案:

watch(id, async (newId, _, onCleanup) => {
  let isCancelled = false
  onCleanup(() => isCancelled = true)
  
  const data = await fetchData(newId)
  if (!isCancelled) {
    currentData.value = data
  }
})

8.3 无限循环问题

问题: 侦听器中修改依赖数据导致循环

watch(count, (newVal) => {
  count.value = newVal + 1 // 无限循环
})

解决方案:

// 添加条件判断
watch(count, (newVal, oldVal) => {
  if (newVal < 100) {
    count.value = newVal + 1
  }
})

// 使用 watchEffect 替代
watchEffect(() => {
  if (count.value < 100) {
    count.value += 1
  }
})

你可能感兴趣的:(前端框架Vue,vue.js,javascript,前端)