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
  • 复杂场景可以组合使用
相关推荐
冴羽15 小时前
今日苹果 App Store 前端源码泄露,赶紧 fork 一份看看
前端·javascript·typescript
蒜香拿铁15 小时前
Angular【router路由】
前端·javascript·angular.js
时间的情敌15 小时前
Vite 大型项目优化方案
vue.js
brzhang15 小时前
读懂 MiniMax Agent 的设计逻辑,然后我复刻了一个MiniMax Agent
前端·后端·架构
西洼工作室15 小时前
高效管理搜索历史:Vue持久化实践
前端·javascript·vue.js
广州华水科技16 小时前
北斗形变监测传感器在水库安全中的应用及技术优势分析
前端
开发者如是说16 小时前
Compose 开发桌面程序的一些问题
前端·架构
旺代16 小时前
Token 存储与安全防护
前端
洋不写bug17 小时前
html实现简历信息填写界面
前端·html
三十_A17 小时前
【无标题】
前端·后端·node.js