Vue3监听属性

Vue 3 监听属性完全指南

本文档从零开始讲解 Vue 3 的监听属性:如何在一段数据变化时自动执行一段逻辑(如请求接口、本地存储、校验表单),包括 watchwatchEffect 的用法、区别、常见场景和易错点,配有大量示例,适合新手系统学习。


一、什么是“监听”?

1.1 为什么需要监听?

页面上经常有这种需求:当某个数据变了,要做一件事。例如:

  • 用户改了“用户名”,要去检查是否已被占用(请求接口)
  • 路由参数 id 变了,要重新拉取该 id 的详情
  • 购物车数量变了,要把最新数据存到本地
  • 某个输入框变了,要做格式校验或防抖请求

这类逻辑不是“算出一个新值”(那用计算属性),而是“发生某件事之后执行一段代码”(副作用)。在 Vue 里,用 监听 来实现:你声明“监听谁、变的时候做什么”,Vue 会在数据变化时自动执行你写的回调。

1.2 监听长什么样?

Vue 3 提供两种方式:

  1. watch:你明确指定“监听哪个数据”,回调里可以拿到新值旧值,还可配置“是否深度监听、是否立即执行一次”等。
  2. watchEffect:你写一段会用到响应式数据的代码,Vue 自动收集这段代码里用到的依赖,只要其中任意一个变了,就重新执行这段代码。

下面先讲 watch,再讲 watchEffect,最后对比“什么时候用谁”。


二、watch:监听一个或多个数据源

2.1 基本语法

语法: watch(数据源, 回调函数, 可选配置)

  • 数据源:可以是一个 ref、一个 getter 函数 () => xxx、或一个数组(多个数据源)。
  • 回调函数(新值, 旧值) => { ... },在数据变化时执行。
  • 可选配置:如 { deep: true, immediate: true } 等。

返回值:一个停止监听的函数,调用后就不再监听。

2.2 监听一个 ref

<template>
  <div>
    <input v-model="keyword" placeholder="输入关键词" />
    <p>你输入了:{{ keyword }}</p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')

watch(keyword, (newVal, oldVal) => {
  console.log('从', oldVal, '变成了', newVal)
})
</script>
  • keyword 变化时(例如输入或删除字符),回调会执行。
  • newVal:当前最新值;oldVal:上一次的值(首次执行时可能是 undefined,见下文 immediate)。

注意:<script setup> 里,若数据源是 ref,直接传 keyword 即可(不要传 keyword.value),watch 会监听这个 ref 本身。

2.3 监听 getter(监听对象里的某个属性)

若想监听 对象里某一个属性,数据源要写成 getter 函数,返回你要监听的属性值:

<script setup>
import { ref, watch } from 'vue'
const user = ref({
  name: '小明',
  age: 18
})

// 只监听 user.name 的变化
watch(
  () => user.value.name,
  (newVal, oldVal) => {
    console.log('name 从', oldVal, '变成', newVal)
  }
)
</script>

这样只有 user.name 变化时才会触发,改 user.age 不会触发。

2.4 监听多个数据源(数组)

第一个参数传数组,回调的新值、旧值也是数组,顺序与数据源一致:

<script setup>
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')

watch(
  [firstName, lastName],
  ([newFirst, newLast], [oldFirst, oldLast]) => {
    console.log('姓从', oldFirst, '变成', newFirst)
    console.log('名从', oldLast, '变成', newLast)
  }
)
</script>

任一一个 ref 变化,回调都会执行;回调里通过解构拿到每个的新值和旧值。


三、watch 的常用配置项

3.1 deep:深度监听(对象、数组内部变化)

默认情况下,watch 只监听数据源本身的替换(例如整个 ref 被赋成新对象)。
若数据源是对象或数组,你改的是里面的属性或元素(如 obj.a = 1arr.push(1)),默认不会触发。

加上 deep: true 后,对象/数组内部的变化也会触发回调:

<script setup>
import { ref, watch } from 'vue'
const form = ref({
  name: '',
  age: 0
})

watch(
  form,
  (newVal, oldVal) => {
    console.log('form 有变化', newVal)
  },
  { deep: true }
)
</script>

此时改 form.value.nameform.value.age 都会触发。
注意: 使用 deep 时,newValoldVal 会指向同一个对象(因为 Vue 监听的是同一引用内部的变化),若需要“旧值”做对比,可以在回调里自己拷贝一份或用其它方式记录。

3.2 immediate:立即执行一次

默认是数据第一次变化时才执行回调;若希望一上来就执行一次(例如根据初始值请求接口),可加 immediate: true

<script setup>
import { ref, watch } from 'vue'
const id = ref(1)

watch(
  id,
  (newVal) => {
    console.log('当前 id:', newVal)
    // 这里可以根据 id 请求详情接口
  },
  { immediate: true }
)
</script>

加了 immediate 后,组件创建时就会执行一次;此时 oldValundefined(因为还没有“上一次”)。

3.3 同时用 deep 和 immediate

<script setup>
import { ref, watch } from 'vue'
const options = ref({ page: 1, size: 10 })

watch(
  options,
  (newVal) => {
    console.log('options 变化,重新请求', newVal)
  },
  { deep: true, immediate: true }
)
</script>

适合“一进来就根据 options 请求一次,之后 options 内部任何属性变了也再请求”的场景。


四、watch 的典型用法小结

数据源写法 含义
watch(ref变量, 回调) 监听整个 ref(值或引用变化都会触发)
watch(() => obj.value.xxx, 回调) 只监听对象某个属性
watch([a, b], 回调) 监听多个源,回调参数为 (新值数组, 旧值数组)
{ deep: true } 监听对象/数组内部变化
{ immediate: true } 创建时先执行一次回调

五、停止监听

watch 会返回一个函数,调用这个函数就停止监听

<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const stop = watch(count, (newVal) => {
  console.log(newVal)
  if (newVal >= 10) {
    stop()  // 到 10 就不再监听了
  }
})
</script>

在组件被卸载时,若希望停止监听,可以在 onUnmounted 里调用这个停止函数(Vue 3 里在 setup 中创建的 watch 会在组件卸载时自动停止,手动 stop 常用于“条件满足就不再监听”等场景)。


六、watchEffect:自动收集依赖

watchEffect 不要求你显式写出“监听谁”,而是执行一个函数,函数里用到的 ref/reactive 会被自动收集为依赖;只要其中任意一个变了,函数会重新执行

6.1 基本语法

语法: watchEffect(回调函数)

  • 回调函数:无参数;里面可以随意使用多个响应式数据。
  • 返回值:同样是停止监听的函数
<template>
  <div>
    <input v-model="keyword" />
    <p>关键词:{{ keyword }}</p>
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue'
const keyword = ref('')

watchEffect(() => {
  console.log('keyword 当前是:', keyword.value)
})
</script>
  • 第一次会执行一次,打印当前 keyword。
  • 之后只要 keyword 变化,就会再执行。
  • 若你在回调里还用了别的 ref(如 count.value),count 也会自动成为依赖,变了也会触发。

6.2 和 watch 的对比(何时用谁)

对比项 watch watchEffect
是否要写数据源 要,明确指定 ref/getter/数组 不用,自动根据回调里用到的依赖
能否拿到旧值 能,(newVal, oldVal) 不能,只关心“当前值”
是否默认执行 默认不执行(可配 immediate) 默认会先执行一次
典型用途 需要旧值、需要精确控制监听谁 依赖多、逻辑简单、不需要旧值

简单记: 需要旧值只监听某几个精确目标watch依赖多、逻辑简单、一进来就要跑一次watchEffect


七、常见使用场景示例

7.1 数据变化时请求接口(如搜索、详情)

<template>
  <div>
    <input v-model="keyword" placeholder="搜索" />
    <ul>
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const list = ref([])

watch(keyword, async (newVal) => {
  if (!newVal.trim()) {
    list.value = []
    return
  }
  const res = await fetch('/api/search?q=' + encodeURIComponent(newVal))
  const data = await res.json()
  list.value = data.list
}, { immediate: false })
</script>

若希望“一进来就按空关键词请求一次”,可加 immediate: true

7.2 路由参数变化时重新拉取详情

<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const detail = ref(null)

watch(
  () => route.params.id,
  async (newId) => {
    if (!newId) return
    const res = await fetch(`/api/item/${newId}`)
    detail.value = await res.json()
  },
  { immediate: true }
)
</script>

getter 监听 route.params.id,并 immediate: true,进入页或 id 变化都会请求。

7.3 同步到本地存储(localStorage)

<script setup>
import { ref, watch } from 'vue'
const settings = ref({
  theme: 'light',
  fontSize: 14
})

watch(
  settings,
  (newVal) => {
    localStorage.setItem('settings', JSON.stringify(newVal))
  },
  { deep: true, immediate: true }
)
</script>

deep: true 保证改 settings 里任意字段都会存;immediate: true 保证一进来就把当前 settings 写进 localStorage(若之前已存过,可先在 onMounted 里从 localStorage 读出来赋给 settings)。

7.4 表单校验(某字段变化时校验)

<template>
  <div>
    <input v-model="form.username" placeholder="用户名" />
    <p v-if="error">{{ error }}</p>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
const form = ref({ username: '' })
const error = ref('')

watch(
  () => form.value.username,
  (newVal) => {
    if (!newVal) {
      error.value = '请输入用户名'
    } else if (newVal.length < 3) {
      error.value = '至少 3 个字符'
    } else {
      error.value = ''
    }
  }
)
</script>

7.5 防抖(输入停止一段时间后再请求)

<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
let timer = null

watch(keyword, (newVal) => {
  clearTimeout(timer)
  timer = setTimeout(() => {
    console.log('防抖后请求:', newVal)
    // 这里写请求逻辑
  }, 300)
})
</script>

onUnmounted 里最好 clearTimeout(timer),避免组件销毁后仍执行。

7.6 用 watchEffect 做“一进来就执行且依赖多个数据”

<script setup>
import { ref, watchEffect } from 'vue'
const page = ref(1)
const size = ref(10)
const list = ref([])

watchEffect(async () => {
  const res = await fetch(`/api/list?page=${page.value}&size=${size.value}`)
  list.value = await res.json()
})
</script>

pagesize 任一变化都会重新请求;并且一进来就会执行一次。


八、易错点与注意点

8.1 避免在监听回调里改被监听的数据(死循环)

若在 watchwatchEffect 的回调里又修改了被监听的数据,会再次触发监听,可能造成无限循环
例如:

watch(count, () => {
  count.value++  // 危险:count 变了又触发 watch,又 ++ ...
})

需要“数据 A 变时改数据 B、数据 B 变时改数据 A”时,要加条件或标志位,避免互相触发。

8.2 监听“对象整体”时,newVal 和 oldVal 可能相同

使用 deep: true 监听对象时,回调里的 newValoldVal 会指向同一个对象(当前引用),因为 Vue 监听的是引用内部变化。若需要“旧值”做对比,可在回调内用 拷贝 或自己维护一份旧值。

8.3 watchEffect 会立即执行一次

若你有一段“只想在依赖变化时跑、首次不跑”的逻辑,用 watch不要immediate;若用 watchEffect,可在回调里加条件判断,或改用 watch。


九、监听属性速查表

需求 写法
监听一个 ref watch(ref名, (newVal, oldVal) => { })
监听对象某属性 watch(() => obj.value.xxx, 回调)
监听多个 watch([a, b], ([newA, newB], [oldA, oldB]) => { })
深度监听 第三个参数 { deep: true }
立即执行一次 第三个参数 { immediate: true }
自动收集依赖、立即执行 watchEffect(() => { ... })
停止监听 使用 watch/watchEffect 的返回值:const stop = watch(...); stop()

十、学习建议

  1. 先掌握 watch(数据源, 回调),能监听一个 ref、一个 getter、以及 deep / immediate 的用法。
  2. 理解 watchwatchEffect 的区别:要不要旧值、要不要精确指定数据源、是否默认执行一次。
  3. 常见用法:数据变→请求接口、路由参数变→拉详情、表单变→校验或存 localStorage、防抖请求。
  4. 避免在回调里修改被监听的数据导致死循环;需要“互相联动”时加条件或拆分逻辑。

把本文档里的示例在项目里敲一遍、改数据看回调是否触发,会掌握得更牢。祝你学习顺利。

发表评论