Vue 3 watch 与 watchEffect ,哪个更好?

Vue 3 watch 与 watchEffect ,哪个更好?

文章目录

  • [Vue 3 watch 与 watchEffect ,哪个更好?](#Vue 3 watch 与 watchEffect ,哪个更好?)
    • [1. 概述](#1. 概述)
    • [2. watch 详解](#2. watch 详解)
      • [2.1 基本语法](#2.1 基本语法)
      • [2.2 基本用法](#2.2 基本用法)
      • [2.3 侦听响应式对象](#2.3 侦听响应式对象)
      • [2.4 高级用法](#2.4 高级用法)
    • [3. watchEffect 详解](#3. watchEffect 详解)
      • [3.1 基本语法](#3.1 基本语法)
      • [3.2 基本用法](#3.2 基本用法)
      • [3.3 实际应用场景](#3.3 实际应用场景)
    • [4. 详细比较](#4. 详细比较)
      • [4.1 语法和用法对比](#4.1 语法和用法对比)
      • [4.2 代码对比示例](#4.2 代码对比示例)
      • [4.3 性能考虑示例](#4.3 性能考虑示例)
    • [5. 高级功能和选项](#5. 高级功能和选项)
      • [5.1 刷新时机控制](#5.1 刷新时机控制)
      • [5.2 调试功能](#5.2 调试功能)
    • [6. 实际应用场景](#6. 实际应用场景)
      • [6.1 表单验证](#6.1 表单验证)
      • [6.2 API 数据获取](#6.2 API 数据获取)
    • [7. 最佳实践和性能优化](#7. 最佳实践和性能优化)
      • [7.1 何时使用哪个](#7.1 何时使用哪个)
      • [7.2 性能优化技巧](#7.2 性能优化技巧)
    • [8. 总结](#8. 总结)
      • [watch 特点:](#watch 特点:)
      • [watchEffect 特点:](#watchEffect 特点:)
      • 选择建议:

1. 概述

Vue 3 提供了两种响应式侦听器:watchwatchEffect。它们都用于响应数据变化执行副作用,但在使用方式和场景上有所不同。

2. watch 详解

2.1 基本语法

typescript 复制代码
watch(source, callback, options?)

2.2 基本用法

vue 复制代码
<template>
  <div>
    <input v-model="name" placeholder="姓名" />
    <input v-model="age" type="number" placeholder="年龄" />
    <p>姓名: {{ name }}, 年龄: {{ age }}</p>
  </div>
</template>

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

const name = ref('')
const age = ref(0)

// 侦听单个 ref
watch(name, (newName, oldName) => {
  console.log(`姓名从 "${oldName}" 变为 "${newName}"`)
})

// 侦听多个源
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
  console.log(`姓名: ${oldName} -> ${newName}, 年龄: ${oldAge} -> ${newAge}`)
})

// 立即执行
watch(name, (newName, oldName) => {
  console.log('立即执行,当前姓名:', newName)
}, { immediate: true })

// 深度侦听对象
const user = ref({
  profile: {
    firstName: '张',
    lastName: '三'
  }
})

watch(user, (newUser, oldUser) => {
  console.log('用户信息变化:', newUser)
}, { deep: true })
</script>

2.3 侦听响应式对象

vue 复制代码
<template>
  <div>
    <input v-model="user.firstName" placeholder="名" />
    <input v-model="user.lastName" placeholder="姓" />
    <p>全名: {{ fullName }}</p>
  </div>
</template>

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

interface User {
  firstName: string
  lastName: string
}

const user = reactive<User>({
  firstName: '',
  lastName: ''
})

// 侦听 reactive 对象的特定属性
watch(
  () => user.firstName,
  (newFirstName, oldFirstName) => {
    console.log(`名从 "${oldFirstName}" 变为 "${newFirstName}"`)
  }
)

// 侦听多个属性
watch(
  [() => user.firstName, () => user.lastName],
  ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
    console.log(`姓名变化: ${oldFirstName}${oldLastName} -> ${newFirstName}${newLastName}`)
  }
)

const fullName = computed(() => `${user.firstName}${user.lastName}`)
</script>

2.4 高级用法

vue 复制代码
<template>
  <div>
    <input v-model="searchQuery" placeholder="搜索..." />
    <div v-if="loading">搜索中...</div>
    <ul v-else>
      <li v-for="result in searchResults" :key="result.id">
        {{ result.name }}
      </li>
    </ul>
  </div>
</template>

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

interface SearchResult {
  id: number
  name: string
}

const searchQuery = ref('')
const searchResults = ref<SearchResult[]>([])
const loading = ref(false)

// 防抖搜索
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
  if (!newQuery.trim()) {
    searchResults.value = []
    return
  }

  loading.value = true
  
  // 取消之前的请求
  let cancelled = false
  onCleanup(() => {
    cancelled = true
  })

  try {
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 500))
    
    if (cancelled) return
    
    // 模拟搜索结果
    searchResults.value = [
      { id: 1, name: `${newQuery} 结果1` },
      { id: 2, name: `${newQuery} 结果2` },
      { id: 3, name: `${newQuery} 结果3` }
    ]
  } catch (error) {
    console.error('搜索失败:', error)
  } finally {
    if (!cancelled) {
      loading.value = false
    }
  }
}, { immediate: true })
</script>

3. watchEffect 详解

3.1 基本语法

typescript 复制代码
watchEffect(effect, options?)

3.2 基本用法

vue 复制代码
<template>
  <div>
    <input v-model="count" type="number" />
    <input v-model="multiplier" type="number" />
    <p>结果: {{ result }}</p>
  </div>
</template>

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

const count = ref(1)
const multiplier = ref(2)
const result = ref(0)

// 自动追踪依赖
watchEffect(() => {
  result.value = count.value * multiplier.value
  console.log(`计算: ${count.value} * ${multiplier.value} = ${result.value}`)
})

// 副作用清理
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('延迟执行的操作')
  }, 1000)
  
  onCleanup(() => {
    clearTimeout(timer)
    console.log('清理定时器')
  })
})
</script>

3.3 实际应用场景

vue 复制代码
<template>
  <div>
    <input v-model="url" placeholder="图片URL" />
    <div v-if="loading">加载中...</div>
    <img v-else :src="blobUrl" alt="预览" style="max-width: 300px;" />
  </div>
</template>

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

const url = ref('')
const blobUrl = ref('')
const loading = ref(false)

watchEffect(async (onCleanup) => {
  if (!url.value) {
    blobUrl.value = ''
    return
  }

  loading.value = true
  let cancelled = false
  
  onCleanup(() => {
    cancelled = true
    loading.value = false
  })

  try {
    // 模拟图片加载
    const response = await fetch(url.value)
    if (cancelled) return
    
    const blob = await response.blob()
    if (cancelled) return
    
    // 创建对象 URL
    const newBlobUrl = URL.createObjectURL(blob)
    
    // 清理之前的 URL
    if (blobUrl.value) {
      URL.revokeObjectURL(blobUrl.value)
    }
    
    blobUrl.value = newBlobUrl
  } catch (error) {
    console.error('图片加载失败:', error)
    blobUrl.value = ''
  } finally {
    if (!cancelled) {
      loading.value = false
    }
  }
})
</script>

4. 详细比较

4.1 语法和用法对比

特性 watch watchEffect
依赖收集 显式声明侦听源 自动收集依赖
初始执行 需要 immediate: true 立即执行
新旧值 提供新旧值 不提供新旧值
性能 精确控制侦听源 可能过度触发
使用场景 需要精确控制时 响应式副作用

4.2 代码对比示例

vue 复制代码
<template>
  <div>
    <input v-model="firstName" placeholder="名" />
    <input v-model="lastName" placeholder="姓" />
    <p>使用 watch: {{ fullNameWatch }}</p>
    <p>使用 watchEffect: {{ fullNameEffect }}</p>
  </div>
</template>

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

const firstName = ref('')
const lastName = ref('')
const fullNameWatch = ref('')
const fullNameEffect = ref('')

// 使用 watch - 需要显式声明依赖
watch(
  [firstName, lastName],
  ([newFirst, newLast]) => {
    fullNameWatch.value = `${newFirst} ${newLast}`
    console.log('watch - 全名更新:', fullNameWatch.value)
  },
  { immediate: true }
)

// 使用 watchEffect - 自动收集依赖
watchEffect(() => {
  fullNameEffect.value = `${firstName.value} ${lastName.value}`
  console.log('watchEffect - 全名更新:', fullNameEffect.value)
})
</script>

4.3 性能考虑示例

vue 复制代码
<template>
  <div>
    <input v-model="filter" placeholder="过滤条件" />
    <input v-model="sortBy" placeholder="排序字段" />
    <button @click="addItem">添加项目</button>
    <ul>
      <li v-for="item in displayedItems" :key="item.id">
        {{ item.name }} - {{ item.value }}
      </li>
    </ul>
  </div>
</template>

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

interface ListItem {
  id: number
  name: string
  value: number
}

const items = ref<ListItem[]>([
  { id: 1, name: '项目A', value: 10 },
  { id: 2, name: '项目B', value: 20 },
  { id: 3, name: '项目C', value: 30 }
])

const filter = ref('')
const sortBy = ref<'name' | 'value'>('name')

// 使用 computed - 最佳性能
const displayedItems = computed(() => {
  console.log('computed 执行')
  let result = [...items.value]
  
  if (filter.value) {
    result = result.filter(item => 
      item.name.toLowerCase().includes(filter.value.toLowerCase())
    )
  }
  
  if (sortBy.value) {
    result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
  }
  
  return result
})

// 使用 watch - 精确控制
const filteredItems = ref<ListItem[]>([])
watch(
  [() => items.value, filter, sortBy],
  () => {
    console.log('watch 执行')
    let result = [...items.value]
    
    if (filter.value) {
      result = result.filter(item => 
        item.name.toLowerCase().includes(filter.value.toLowerCase())
      )
    }
    
    if (sortBy.value) {
      result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
    }
    
    filteredItems.value = result
  },
  { immediate: true, deep: true }
)

// 使用 watchEffect - 可能过度触发
const effectItems = ref<ListItem[]>([])
watchEffect(() => {
  console.log('watchEffect 执行')
  let result = [...items.value]
  
  if (filter.value) {
    result = result.filter(item => 
      item.name.toLowerCase().includes(filter.value.toLowerCase())
    )
  }
  
  if (sortBy.value) {
    result.sort((a, b) => a[sortBy.value].localeCompare?.(b[sortBy.value]) || a[sortBy.value] - b[sortBy.value])
  }
  
  effectItems.value = result
})

const addItem = () => {
  const newId = Math.max(...items.value.map(i => i.id)) + 1
  items.value.push({
    id: newId,
    name: `项目${String.fromCharCode(64 + newId)}`,
    value: Math.random() * 100
  })
}
</script>

5. 高级功能和选项

5.1 刷新时机控制

vue 复制代码
<template>
  <div>
    <input v-model="text" placeholder="输入文本" />
    <p>处理后的文本: {{ processedText }}</p>
  </div>
</template>

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

const text = ref('')
const processedText = ref('')

// pre: DOM 更新前执行
watch(text, (newText) => {
  processedText.value = newText.toUpperCase()
  console.log('DOM 更新前执行')
}, { flush: 'pre' })

// post: DOM 更新后执行
watch(text, (newText) => {
  console.log('DOM 更新后执行, 可以安全访问 DOM')
}, { flush: 'post' })

// sync: 同步执行(不推荐)
watch(text, (newText) => {
  console.log('同步执行')
}, { flush: 'sync' })

// watchEffect 同样支持 flush 选项
watchEffect(() => {
  console.log('当前文本:', text.value)
}, { flush: 'post' })
</script>

5.2 调试功能

vue 复制代码
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'

const debugValue = ref('')

// 调试 watch
watch(debugValue, (newVal, oldVal) => {
  console.log('值变化:', oldVal, '->', newVal)
}, {
  onTrack: (e) => {
    console.log('依赖被追踪', e)
  },
  onTrigger: (e) => {
    console.log('依赖被触发', e)
  }
})

// 调试 watchEffect
watchEffect(() => {
  console.log('当前值:', debugValue.value)
}, {
  onTrack: (e) => {
    console.log('effect 依赖被追踪', e)
  },
  onTrigger: (e) => {
    console.log('effect 依赖被触发', e)
  }
})
</script>

6. 实际应用场景

6.1 表单验证

vue 复制代码
<template>
  <form @submit="handleSubmit">
    <input v-model="email" placeholder="邮箱" />
    <span v-if="emailError" class="error">{{ emailError }}</span>
    
    <input v-model="password" type="password" placeholder="密码" />
    <span v-if="passwordError" class="error">{{ passwordError }}</span>
    
    <button type="submit" :disabled="!isFormValid">提交</button>
  </form>
</template>

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

const email = ref('')
const password = ref('')
const emailError = ref('')
const passwordError = ref('')
const isFormValid = ref(false)

// 使用 watch 进行表单验证
watch(email, (newEmail) => {
  if (!newEmail) {
    emailError.value = '邮箱不能为空'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
    emailError.value = '邮箱格式不正确'
  } else {
    emailError.value = ''
  }
}, { immediate: true })

watch(password, (newPassword) => {
  if (!newPassword) {
    passwordError.value = '密码不能为空'
  } else if (newPassword.length < 6) {
    passwordError.value = '密码至少6位'
  } else {
    passwordError.value = ''
  }
}, { immediate: true })

// 使用 watchEffect 检查表单整体有效性
watchEffect(() => {
  isFormValid.value = !emailError.value && !passwordError.value && 
                      email.value.length > 0 && password.value.length > 0
})

const handleSubmit = (e: Event) => {
  e.preventDefault()
  if (isFormValid.value) {
    console.log('表单提交:', { email: email.value, password: password.value })
  }
}
</script>

<style scoped>
.error {
  color: red;
  font-size: 12px;
}
</style>

6.2 API 数据获取

vue 复制代码
<template>
  <div>
    <select v-model="selectedCategory">
      <option value="">所有分类</option>
      <option value="electronics">电子产品</option>
      <option value="clothing">服装</option>
      <option value="books">图书</option>
    </select>
    
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error }}</div>
    <ul v-else>
      <li v-for="product in products" :key="product.id">
        {{ product.name }} - ${{ product.price }}
      </li>
    </ul>
  </div>
</template>

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

interface Product {
  id: number
  name: string
  price: number
  category: string
}

const selectedCategory = ref('')
const products = ref<Product[]>([])
const loading = ref(false)
const error = ref('')

// 使用 watch 处理 API 调用
watch(selectedCategory, async (newCategory, oldCategory, onCleanup) => {
  loading.value = true
  error.value = ''
  
  let cancelled = false
  onCleanup(() => {
    cancelled = true
  })

  try {
    // 模拟 API 调用
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    if (cancelled) return
    
    // 模拟数据
    const mockProducts: Product[] = [
      { id: 1, name: 'iPhone', price: 999, category: 'electronics' },
      { id: 2, name: 'T-shirt', price: 29, category: 'clothing' },
      { id: 3, name: 'Vue.js Guide', price: 39, category: 'books' },
      { id: 4, name: 'MacBook', price: 1999, category: 'electronics' },
      { id: 5, name: 'Jeans', price: 59, category: 'clothing' }
    ]
    
    if (newCategory) {
      products.value = mockProducts.filter(p => p.category === newCategory)
    } else {
      products.value = mockProducts
    }
  } catch (err) {
    error.value = '加载失败'
    console.error('API 错误:', err)
  } finally {
    if (!cancelled) {
      loading.value = false
    }
  }
}, { immediate: true })
</script>

7. 最佳实践和性能优化

7.1 何时使用哪个

使用 watch 的情况:

  • 需要访问旧值和新值
  • 需要精确控制侦听的源
  • 需要懒执行(非立即执行)
  • 处理昂贵的操作需要防抖

使用 watchEffect 的情况:

  • 简单的响应式副作用
  • 依赖关系复杂或动态变化
  • 需要立即执行
  • 逻辑简单,不需要旧值

7.2 性能优化技巧

vue 复制代码
<script setup lang="ts">
import { ref, watch, watchEffect, computed } from 'vue'

const expensiveData = ref({/* 大量数据 */})
const filterCondition = ref('')

// 不好的做法:深度侦听大量数据
watch(expensiveData, () => {
  // 每次 expensiveData 的任何变化都会触发
}, { deep: true })

// 好的做法:精确侦听需要的部分
watch(
  () => expensiveData.value.someSpecificProperty,
  () => {
    // 只有特定属性变化时触发
  }
)

// 使用 computed 进行复杂计算
const filteredData = computed(() => {
  return expensiveData.value.filter(item => 
    item.name.includes(filterCondition.value)
  )
})

// 只在必要时执行
const shouldWatch = ref(false)
watch(
  () => shouldWatch.value ? expensiveData.value : null,
  () => {
    // 只有 shouldWatch 为 true 时执行
  }
)
</script>

8. 总结

watch 特点:

  • 精确控制:显式声明侦听源
  • 访问旧值:可以比较变化前后的值
  • 灵活配置:支持 immediate、deep 等选项
  • 性能优化:避免不必要的执行

watchEffect 特点:

  • 自动追踪:自动收集响应式依赖
  • 立即执行:创建后立即运行一次
  • 简洁语法:不需要显式声明依赖
  • 动态依赖:依赖关系可以动态变化

选择建议:

  • 大多数情况下,优先考虑使用 computed
  • 需要副作用且依赖明确时使用 watch
  • 简单的响应式副作用使用 watchEffect
  • 复杂场景可以组合使用
相关推荐
Z***u65921 分钟前
前端性能测试实践
前端
xhxxx24 分钟前
prototype 是遗产,proto 是族谱:一文吃透 JS 原型链
前端·javascript
倾墨25 分钟前
Bytebot源码学习
前端
用户938169125536028 分钟前
VUE3项目--集成Sass
前端
QuantumLeap丶34 分钟前
《Flutter全栈开发实战指南:从零到高级》- 19 -手势识别
flutter·ios·前端框架
S***H28339 分钟前
Vue语音识别案例
前端·vue.js·语音识别
涔溪1 小时前
通过Nginx反向代理配置连接多个后端服务器
vue.js·nginx
啦啦9118861 小时前
【版本更新】Edge 浏览器 v142.0.3595.94 绿色增强版+官方安装包
前端·edge
蚂蚁集团数据体验技术2 小时前
一个可以补充 Mermaid 的可视化组件库 Infographic
前端·javascript·llm
LQW_home2 小时前
前端展示 接受springboot Flux数据demo
前端·css·css3