1. watch 对基本类型 vs 对象类型的区别

1.1 监听基本类型(ref)

<template>
  <div>
    <h2>监听基本类型 (ref)</h2>
    <p>当前计数: {{ count }}</p>
    <button @click="count++">+1</button>
    <button @click="count--">-1</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

// ✅ 直接监听 ref,不需要 .value
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
</script>

要点:

  • 基本类型用 ref() 包装
  • watch 直接传入 ref 变量本身(不加 .value
  • 当值改变时,watch 会自动触发

1.2 监听对象类型(reactive)

<template>
  <div>
    <h2>监听对象类型 (reactive)</h2>
    <p>姓名: {{ user.name }}</p>
    <p>年龄: {{ user.age }}</p>
    <button @click="user.age++">年龄 +1</button>
    <button @click="user.name = '小红'">改名为小红</button>
  </div>
</template>
<script setup>
import { reactive, watch } from 'vue'

const user = reactive({
  name: '小明',
  age: 18
})

// ⚠️ 监听整个 reactive 对象(默认就是深度监听)
watch(user, (newVal, oldVal) => {
  // 注意:newVal 和 oldVal 是同一个对象引用
  console.log('user 变化了', newVal)
  console.log('newVal === oldVal:', newVal === oldVal) // true
})
</script>

重要区别:

特性

基本类型 (ref)

对象类型 (reactive)

监听方式

直接传入 ref

直接传入 reactive 对象

默认深度监听

是(自动 deep)

oldVal 准确性

✅ 准确

❌ 通常与 newVal 相同


1.3 监听对象的某个属性

<template>
  <div>
    <h2>监听对象的某个属性</h2>
    <p>姓名: {{ user.name }}</p>
    <p>年龄: {{ user.age }}</p>
    <button @click="user.age++">年龄 +1</button>
    <button @click="user.name = '小红'">改名为小红</button>
  </div>
</template>
<script setup>
import { reactive, watch } from 'vue'

const user = reactive({
  name: '小明',
  age: 18
})

// ✅ 监听对象的某个属性,需要用函数返回
watch(
  () => user.age,  // 👈 必须用箭头函数
  (newAge, oldAge) => {
    console.log(`年龄从 ${oldAge} 变为 ${newAge}`)
  }
)

// 也可以同时监听多个属性
watch(
  () => user.name,
  (newName, oldName) => {
    console.log(`名字从 ${oldName} 变为 ${newName}`)
  }
)
</script>

为什么要用函数?

  • user.age 是一个普通值(如 18),不是响应式的
  • () => user.age 让 Vue 每次重新获取最新值

1.4 监听 ref 包装的对象

<template>
  <div>
    <h2>监听 ref 包装的对象</h2>
    <p>姓名: {{ user.name }}</p>
    <p>年龄: {{ user.age }}</p>
    <button @click="user.age++">年龄 +1</button>
    <button @click="replaceUser">替换整个对象</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const user = ref({
  name: '小明',
  age: 18
})

// ❌ 这样不会监听到内部属性变化(只有整体替换才触发)
watch(user, (newVal) => {
  console.log('user 整体被替换了', newVal)
})

// ✅ 方法1:添加 deep 选项(监听内部变化)
watch(user, (newVal) => {
  console.log('user 内部变化了(deep)', newVal)
}, { deep: true })

// ✅ 方法2:监听具体属性
watch(
  () => user.value.age,
  (newAge, oldAge) => {
    console.log(`age 从 ${oldAge} 变为 ${newAge}`)
  }
)

// 替换整个对象
function replaceUser() {
  user.value = { name: '小红', age: 20 }
}
</script>

2. 什么时候使用 deep

2.1 deep 的作用

<template>
  <div>
    <h2>deep 深度监听示例</h2>
    <p>城市: {{ state.user.profile.address.city }}</p>
    <button @click="changeCity">修改城市</button>
    <button @click="replaceState">替换整个 state</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const state = ref({
  user: {
    profile: {
      address: {
        city: '北京'
      }
    }
  }
})

// 没有 deep:只有 state.value 整体被替换才触发
watch(state, (newVal) => {
  console.log('【无deep】state 变化了')
})

// 有 deep:任何嵌套属性变化都会触发
watch(state, (newVal) => {
  console.log('【有deep】state 变化了,城市是:', newVal.user.profile.address.city)
}, { deep: true })

function changeCity() {
  state.value.user.profile.address.city = '上海'
}

function replaceState() {
  state.value = {
    user: {
      profile: {
        address: {
          city: '广州'
        }
      }
    }
  }
}
</script>

2.2 何时需要 deep

场景

是否需要 deep

ref 包装的对象,想监听内部变化

✅ 需要

reactive 对象

❌ 不需要(默认就是深度监听)

只关心对象的某个特定属性

❌ 不需要(用函数返回该属性)

嵌套很深的对象

✅ 需要(但要注意性能)


2.3 deep 的性能优化

<template>
  <div>
    <h2>deep 性能优化</h2>
    <p>选中的项目: {{ selectedItem?.name }}</p>
    <ul>
      <li v-for="item in bigData.items" :key="item.id">
        {{ item.name }}
        <button @click="selectItem(item)">选择</button>
      </li>
    </ul>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const bigData = ref({
  items: [
    { id: 1, name: '项目A', data: { /* 大量数据 */ } },
    { id: 2, name: '项目B', data: { /* 大量数据 */ } },
    { id: 3, name: '项目C', data: { /* 大量数据 */ } }
  ],
  selectedId: null
})

const selectedItem = ref(null)

// 🚫 不推荐:深度监听大对象(性能差)
// watch(bigData, () => { ... }, { deep: true })

// ✅ 推荐:只监听你关心的属性
watch(
  () => bigData.value.selectedId,
  (newId) => {
    console.log('选中的ID变化:', newId)
    selectedItem.value = bigData.value.items.find(item => item.id === newId)
  }
)

function selectItem(item) {
  bigData.value.selectedId = item.id
}
</script>

3. watch 的其它属性

3.1 immediate - 立即执行

<template>
  <div>
    <h2>immediate 立即执行</h2>
    <p>用户ID: {{ userId }}</p>
    <p>用户信息: {{ userInfo }}</p>
    <button @click="userId++">切换用户 (ID +1)</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const userId = ref(1)
const userInfo = ref('加载中...')

// 模拟获取用户信息
function fetchUser(id) {
  return `用户${id}的信息`
}

// immediate: true 让回调立即执行一次
watch(userId, (newId) => {
  console.log('获取用户信息,ID:', newId)
  userInfo.value = fetchUser(newId)
}, { immediate: true })

// 页面加载时就会执行一次,不需要等 userId 变化
</script>

使用场景:

  • 页面初始化时需要根据数据执行某些操作
  • 需要立即获取初始值进行处理

3.2 flush - 执行时机

<template>
  <div>
    <h2>flush 执行时机</h2>
    <p ref="countEl">计数: {{ count }}</p>
    <button @click="count++">+1</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
const countEl = ref(null)

// flush: 'pre' (默认) - DOM 更新前执行
watch(count, (newVal) => {
  console.log('【pre】新值:', newVal)
  console.log('【pre】DOM显示:', countEl.value?.textContent)
  // DOM 还没更新,显示的是旧值
})

// flush: 'post' - DOM 更新后执行
watch(count, (newVal) => {
  console.log('【post】新值:', newVal)
  console.log('【post】DOM显示:', countEl.value?.textContent)
  // DOM 已更新,显示的是新值
}, { flush: 'post' })
</script>

flush 选项说明:

  • 'pre'(默认):DOM 更新前执行
  • 'post':DOM 更新后执行(适合需要访问更新后的 DOM)
  • 'sync':同步执行(谨慎使用,可能影响性能)

3.3 停止监听

<template>
  <div>
    <h2>停止监听</h2>
    <p>计数: {{ count }}</p>
    <p>监听状态: {{ isWatching ? '监听中' : '已停止' }}</p>
    <button @click="count++">+1</button>
    <button @click="toggleWatch">
      {{ isWatching ? '停止监听' : '开始监听' }}
    </button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
const isWatching = ref(true)

let stopWatch = null

// 创建监听器并保存停止函数
function createWatch() {
  stopWatch = watch(count, (newVal, oldVal) => {
    console.log(`count 从 ${oldVal} 变为 ${newVal}`)
  })
  isWatching.value = true
}

// 初始创建
createWatch()

function toggleWatch() {
  if (isWatching.value) {
    // 停止监听
    stopWatch()
    isWatching.value = false
    console.log('已停止监听')
  } else {
    // 重新开始监听
    createWatch()
    console.log('已开始监听')
  }
}
</script>

3.4 监听多个数据源

<template>
  <div>
    <h2>监听多个数据源</h2>
    <p>姓名: {{ name }}</p>
    <p>年龄: {{ age }}</p>
    <p>完整信息: {{ fullInfo }}</p>
    <button @click="name = '小红'">改名</button>
    <button @click="age++">年龄+1</button>
    <button @click="changeBoth">同时修改</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const name = ref('小明')
const age = ref(18)
const fullInfo = ref('')

// 监听多个数据源(数组形式)
watch(
  [name, age],  // 👈 数组形式
  ([newName, newAge], [oldName, oldAge]) => {
    console.log(`名字:${oldName} → ${newName}`)
    console.log(`年龄:${oldAge} → ${newAge}`)
    fullInfo.value = `${newName}, ${newAge}岁`
  },
  { immediate: true }
)

function changeBoth() {
  name.value = '小刚'
  age.value = 25
  // 注意:虽然修改了两个值,但回调只会执行一次
}
</script>

3.5 once - 只执行一次(Vue 3.4+)

<template>
  <div>
    <h2>once 只执行一次</h2>
    <p>计数: {{ count }}</p>
    <p>首次变化记录: {{ firstChange }}</p>
    <button @click="count++">+1</button>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)
const firstChange = ref('等待首次变化...')

// 只在第一次变化时执行,之后自动停止
watch(count, (newVal, oldVal) => {
  console.log('这条日志只会出现一次')
  firstChange.value = `首次变化:${oldVal} → ${newVal}`
}, { once: true })
</script>

4. computed vs watchEffect vs watch 的区别

4.1 computed - 计算属性

<template>
  <div>
    <h2>computed 计算属性</h2>
    <p>单价: <input v-model.number="price" type="number" /></p>
    <p>数量: <input v-model.number="quantity" type="number" /></p>
    <p>总价: {{ total }}</p>
    <p>折后价 (8折): {{ discountedTotal }}</p>
  </div>
</template>
<script setup>
import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(2)

// computed 返回一个计算后的值
// 有缓存:只有依赖变化时才重新计算
const total = computed(() => {
  console.log('计算 total')  // 只有 price 或 quantity 变化才打印
  return price.value * quantity.value
})

const discountedTotal = computed(() => {
  console.log('计算 discountedTotal')
  return total.value * 0.8
})

// 特点:
// ✅ 有缓存:依赖不变时不会重新计算
// ✅ 自动追踪依赖
// ✅ 返回一个 ref
// ❌ 不应该有副作用(不要在里面发请求、修改 DOM)
</script>

4.2 watch - 侦听器

<template>
  <div>
    <h2>watch 侦听器</h2>
    <p>搜索关键词: <input v-model="keyword" /></p>
    <p>搜索结果: {{ result }}</p>
    <p>搜索状态: {{ status }}</p>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')
const result = ref('')
const status = ref('等待输入...')

// 模拟搜索API
function mockSearch(kw) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`"${kw}" 的搜索结果`)
    }, 500)
  })
}

// watch:手动指定要监听的数据,执行副作用
watch(keyword, async (newKeyword, oldKeyword) => {
  console.log(`关键词从 "${oldKeyword}" 变为 "${newKeyword}"`)
  
  if (!newKeyword) {
    result.value = ''
    status.value = '等待输入...'
    return
  }
  
  status.value = '搜索中...'
  
  // 副作用:发起网络请求
  result.value = await mockSearch(newKeyword)
  status.value = '搜索完成'
})

// 特点:
// ✅ 可以获取新值和旧值
// ✅ 可以配置 immediate、deep 等选项
// ❌ 需要手动指定监听的数据源
</script>

4.3 watchEffect - 自动追踪的侦听器

<template>
  <div>
    <h2>watchEffect 自动追踪</h2>
    <p>用户ID: <input v-model.number="userId" type="number" /></p>
    <p>包含详情: <input v-model="includeDetails" type="checkbox" /></p>
    <p>请求URL: {{ requestUrl }}</p>
    <p>结果: {{ userData }}</p>
  </div>
</template>
<script setup>
import { ref, watchEffect } from 'vue'

const userId = ref(1)
const includeDetails = ref(false)
const userData = ref('')
const requestUrl = ref('')

// 模拟API请求
function mockFetch(url) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(`请求 ${url} 的结果`)
    }, 300)
  })
}

// watchEffect:自动追踪函数内用到的所有响应式变量
watchEffect(async () => {
  // Vue 自动知道这里依赖了 userId 和 includeDetails
  const url = `/api/user/${userId.value}${includeDetails.value ? '?details=true' : ''}`
  requestUrl.value = url
  
  console.log('发起请求:', url)
  userData.value = '加载中...'
  userData.value = await mockFetch(url)
})

// 特点:
// ✅ 自动追踪依赖,不用手动指定
// ✅ 立即执行(相当于 immediate: true)
// ❌ 无法获取旧值
// ❌ 依赖不明确,可能追踪到不需要的变量
</script>

4.4 三者对比示例

<template>
  <div>
    <h2>computed vs watch vs watchEffect 对比</h2>
    
    <p>firstName: <input v-model="firstName" /></p>
    <p>lastName: <input v-model="lastName" /></p>
    
    <hr />
    
    <p><strong>computed 结果:</strong> {{ fullNameComputed }}</p>
    <p><strong>watch 结果:</strong> {{ fullNameWatch }}</p>
    <p><strong>watchEffect 结果:</strong> {{ fullNameWatchEffect }}</p>
    
    <hr />
    
    <p>查看控制台观察执行时机</p>
  </div>
</template>
<script setup>
import { ref, computed, watch, watchEffect } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// ========== computed ==========
// 用于:计算派生数据
// 特点:有缓存、有返回值、自动追踪依赖
const fullNameComputed = computed(() => {
  console.log('【computed】执行计算')
  return firstName.value + lastName.value
})

// ========== watch ==========
// 用于:监听变化执行副作用
// 特点:手动指定依赖、可获取新旧值、无返回值
const fullNameWatch = ref('')
watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log('【watch】检测到变化')
    console.log(`  firstName: ${oldFirst} → ${newFirst}`)
    console.log(`  lastName: ${oldLast} → ${newLast}`)
    fullNameWatch.value = newFirst + newLast
  },
  { immediate: true }
)

// ========== watchEffect ==========
// 用于:自动追踪依赖并执行副作用
// 特点:自动追踪、立即执行、无法获取旧值
const fullNameWatchEffect = ref('')
watchEffect(() => {
  console.log('【watchEffect】执行')
  fullNameWatchEffect.value = firstName.value + lastName.value
})
</script>

4.5 如何选择?

需要计算派生数据?
    └─ 是 → 用 computed

需要执行副作用(网络请求、DOM 操作等)?
    └─ 是 → 需要旧值或精确控制?
              └─ 是 → 用 watch
              └─ 否 → 用 watchEffect

5. 什么是副作用

5.1 副作用的定义与示例

<template>
  <div>
    <h2>什么是副作用</h2>
    
    <p>计数: {{ count }}</p>
    <button @click="count++">+1</button>
    
    <hr />
    
    <p>纯函数结果: {{ pureResult }}</p>
    <p>副作用计数器: {{ sideEffectCounter }}</p>
    <p>页面标题已更新,查看浏览器标签</p>
  </div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'

const count = ref(0)
const sideEffectCounter = ref(0)

// ========== 无副作用的函数(纯函数) ==========
// 只计算并返回,不影响外部
function pureAdd(a, b) {
  return a + b
}

const pureResult = computed(() => {
  return pureAdd(count.value, 10)
})

// ========== 有副作用的操作 ==========
watch(count, (newVal) => {
  // 副作用1:修改外部变量
  sideEffectCounter.value++
  
  // 副作用2:修改 DOM(页面标题)
  document.title = `计数: ${newVal}`
  
  // 副作用3:控制台输出
  console.log('count 变化了:', newVal)
  
  // 副作用4:本地存储
  localStorage.setItem('count', String(newVal))
})
</script>

5.2 常见的副作用类型

<template>
  <div>
    <h2>常见副作用类型演示</h2>
    
    <p>搜索: <input v-model="keyword" /></p>
    <button @click="keyword = ''">清空</button>
    
    <hr />
    
    <p>搜索结果: {{ searchResult }}</p>
    <p>本地存储: {{ storedValue }}</p>
  </div>
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'

const keyword = ref('')
const searchResult = ref('')
const storedValue = ref('')

// 读取本地存储
onMounted(() => {
  storedValue.value = localStorage.getItem('lastKeyword') || '无'
})

watch(keyword, async (newKeyword) => {
  if (!newKeyword) {
    searchResult.value = ''
    return
  }

  // ========== 副作用1:网络请求 ==========
  console.log('副作用:发起网络请求')
  // 模拟请求
  await new Promise(resolve => setTimeout(resolve, 300))
  searchResult.value = `"${newKeyword}" 的搜索结果`

  // ========== 副作用2:DOM 操作 ==========
  console.log('副作用:修改页面标题')
  document.title = `搜索: ${newKeyword}`

  // ========== 副作用3:本地存储 ==========
  console.log('副作用:保存到 localStorage')
  localStorage.setItem('lastKeyword', newKeyword)
  storedValue.value = newKeyword

  // ========== 副作用4:控制台输出 ==========
  console.log('副作用:控制台输出 -', newKeyword)
})
</script>

5.3 为什么 computed 不应该有副作用

<template>
  <div>
    <h2>computed 不应有副作用</h2>
    
    <p>单价: <input v-model.number="price" type="number" /></p>
    <p>数量: <input v-model.number="quantity" type="number" /></p>
    
    <hr />
    
    <p>❌ 错误示范的总价: {{ badTotal }}</p>
    <p>✅ 正确示范的总价: {{ goodTotal }}</p>
    <p>请求计数: {{ requestCount }}</p>
  </div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'

const price = ref(100)
const quantity = ref(2)
const requestCount = ref(0)

// ❌ 错误示范:computed 中有副作用
const badTotal = computed(() => {
  // 这些都是副作用,不应该在 computed 中
  console.log('错误:computed 中打印日志')
  requestCount.value++  // 错误:修改其他响应式数据
  // fetch('/api/log')  // 错误:发起网络请求
  
  return price.value * quantity.value
})

// ✅ 正确示范:computed 只做纯计算
const goodTotal = computed(() => {
  return price.value * quantity.value
})

// ✅ 副作用应该放在 watch 中
watch(goodTotal, (newTotal) => {
  console.log('正确:在 watch 中打印日志')
  console.log('总价变化:', newTotal)
  // 这里可以发起请求、修改 DOM 等
})
</script>

为什么 computed 不能有副作用?

  1. computed 可能被多次调用,导致副作用重复执行
  2. 执行时机不确定,难以预测和调试
  3. 违反了 computed「纯计算」的设计初衷

总结

概念

核心要点

watch 基本类型

直接监听 ref,能拿到准确的新旧值

watch 对象类型

reactive 默认深度监听;ref 对象需要 deep: true

deep

深度监听嵌套属性,reactive 默认开启,ref 对象需手动开启

immediate

立即执行一次回调

flush

控制回调执行时机(DOM更新前/后)

computed

计算派生数据,有缓存,不应有副作用

watchEffect

自动追踪依赖,立即执行,处理副作用

watch

手动指定依赖,可配置选项,处理副作用

副作用

函数执行时对外部环境产生的影响(网络请求、DOM操作等)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐