vue3 响应式 API:watch()、watchEffect()

watch()

  • 基本概念
    • watch()用于监视响应式数据的变化,并在数据变化时执行相应的回调函数。
    • 可以监视单个响应式数据、多个响应式数据的组合,或者一个计算属性。
  • 返回值
    • 返回一个函数,调用这个函数可以停止监视。
  • 特点
    • watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
    • watch()可以监视单个数据、多个数据的组合或计算属性。
    • 通过深度监视选项,可以方便地监视对象的内部属性变化。
    • 立即执行选项可以在特定情况下立即执行回调函数,提供更多的控制。

watch()的参数说明

watch()函数接收3个参数:数据源、回调函数、选项对象(可选)。

  • 第一个参数:数据源(被监视的数据)

    • ref()定义的数据:基本类型的响应式变量、对象类型的响应式变量。
    • reactive()定义的数据:对象类型的响应式变量。
    • 一个返回响应式数据的函数,比如一个计算属性的 getter 函数。
    • 一个包含上述内容的数组。
  • 第二个参数:回调函数

    • 当数据源发生变化时,会调用这个回调函数。
    • 回调函数接收三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数onInvalidate
      • 如果只监视一个数据源,那么新值和旧值分别对应数据源变化后的新值和变化前的旧值。
      • 如果监视多个数据源,那么新值和旧值分别是一个包含新数据源值的数组和一个包含旧数据源值的数组。
      • onInvalidate: 这是一个用于注册副作用清理的回调函数。这个回调函数可以在 watch() 的回调函数内部调用,传入一个清理函数作为参数。这个清理函数会在下次 watch() 回调执行之前被调用,以便清理上一次执行产生的副作用。
    • 示例:(newValue, oldValue) => { /* 处理变化的逻辑 */ }
      (newValue, oldValue, onInvalidate) => { /* 处理变化的逻辑, 用于注册副作用清理的回调函数 */ }
  • 第三个参数:选项对象(可选)

    • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
      • 布尔值,默认值为false
      • 如果设置为true,则会进行深度监视,即当监视的是一个对象时,对象的内部属性发生变化也会触发回调函数。
    • immediate:侦听器创建时立即触发回调。第一次调用时旧值是 undefined
      • 布尔值,默认值为false
      • 如果设置为true,则在创建 watch() 时立即调用回调函数,此时旧值是 undefined,新值是当前值。
    • flush:控制回调函数的执行时机。参考回调的刷新时机watchEffect()
      • 默认值是'pre',表示在 DOM 更新之前执行回调;
      • 'post'表示在 DOM 更新之后执行回调;
      • 'sync'表示同步执行回调,即立即执行回调函数,并且在响应式数据变化时同步更新视图。
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
      • onTrack(用于调试):当响应式数据被读取并作为依赖被追踪时,这个函数会被调用。可以在这个函数中记录哪些数据被读取了,以便进行调试和分析依赖关系。
      • onTrigger(用于调试):当响应式数据发生变化并触发依赖时,这个函数会被调用。可以在这个函数中记录哪些数据发生了变化,以便进行调试和分析依赖关系。
    • once:回调函数只会运行一次。侦听器将在回调函数首次运行后自动停止。
      • 布尔值,默认值为false
      • 如果设置为true,回调函数只会执行一次。侦听器将在回调函数首次运行后自动停止。

示例

监视ref()定义的【基本类型】数据

语法: watch(变量名, (newValue, oldValue) => {})

监视时直接写变量名,其本质上监视的是.value

html 复制代码
<template>
  <div>
    <div>count: {{ count }}</div>
    <button @click="addCount">点击 count+1</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

let count = ref(0)

const addCount = () => {
  count.value++
}

// watch 监视的是 ref定义的数据:count
watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`);
})
</script>

使用watch()监视count这个响应式变量的变化。当count的值发生变化时,回调函数会被执行,打印出旧值和新值。

监视ref()定义的【对象类型】数据

语法: watch(变量名, (newValue, oldValue) => {})

监视时直接写变量名,监视的是对象的引用地址值 ,如果想监视对象的内部属性变化,要手动开启深度监视(deep: true

  • 如果修改的是ref()定义的对象中的属性,newValueoldValue 都是新值,因为它们是同一个对象(同一个引用地址)。
  • 如果修改整个ref()定义的对象,newValue 是新值, oldValue 是旧值,因为不是同一个对象了。

示例:

html 复制代码
<template>
  <div>
    <p>姓名: {{ person.name }}</p>
    <p>年龄: {{ person.age }}</p>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

const person = ref({
  name: '张三',
  age: 18
});

const changeName = () => {
  person.value.name += '哈'  
}
const changeAge = () => {
  person.value.age ++   
}

const changePerson = () => {
  person.value = { name: '李四', age: 17 }
}

// changeName、changeAge修改person的内部属性,不会改变person的引用地址,不会触发 watch
// changePerson 对 person 重新赋值,改变了person的引用地址,触发watch
watch(person, (newValue, oldValue) => {
  console.log('newValue: ',  newValue)
  console.log('oldValue: ', oldValue)
})
</script>

如果想要深度监视对象的内部属性变化,可以在watch()第三个参数(选项对象)中设置deep: true

javascript 复制代码
watch(person, (newValue, oldValue) => {
  console.log('newValue: ',  newValue)
  console.log('oldValue: ', oldValue)
},{ deep: true })

开启深度监视(deep: true)后,修改personname属性、age属性,触发了watch()监视。

但是,watch()监视到的newValueoldValue 都是新值:

这是因为newValueoldValue 都是指向同一个引用地址:person的引用地址。

为了更准确地区分新旧值,可以在修改对象内部属性之前,先对旧对象进行一个副本的创建,这样在watch()回调函数中,就可以把副本作为旧值来处理。

监视reactive()定义的【对象类型】数据

监视reactive()定义的对象类型的响应式变量,watch()默认开启了深度监视 ,且这个深度监视是无法关闭的(设置deep: false无效)。

html 复制代码
<template>
  <div>
    <p>姓名: {{ person.name }}</p>
    <p>年龄: {{ person.age }}</p>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
    <button @click="changePerson">修改整个人</button>
  </div>
</template>
<script setup lang="ts">
import { reactive, watch } from 'vue';

const person = reactive({
  name: '张三',
  age: 18
});

const changeName = () => {
  person.name += '哈'  
}
const changeAge = () => {
  person.age ++   
}

const changePerson = () => {
  // Object.assign 方法会合并源对象到目标对象上。
  // 当对一个 reactive 对象使用 Object.assign 时,对象的引用地址不会改变。
  // 它实际上是在修改原始对象的属性,而不是完全替换对象。
  Object.assign(person,{ name: '李四', age: 17 })
}


// 监视reactive()定义的对象类型的响应式变量,watch()默认开启了深度监视。
// changeName、changeAge修改person的内部属性,触发 watch
// changePerson 对 person 重新赋值,改变了person的引用地址,触发watch
watch(person, (newValue, oldValue) => {
  console.log('newValue: ',  newValue)
  console.log('oldValue: ', oldValue)
})
</script>

注意:新值和旧值是同一个值。因为它们都指向同一个引用地址。
changeNamechangeAgechangePerson 从本质上来讲,都是在修改person的属性,没有修改person的引用地址。

监视ref()reactive()定义的【对象类型】数据中的某个属性

  • 若该属性不是 【对象类型】属性,需要写成函数形式
    • 当监视ref()reactive()定义的对象中的某个非对象类型属性时,需要写成函数形式。
    • 因为直接监视一个非对象类型的属性时,watch()无法准确追踪其变化。写成函数形式可以确保正确地获取属性值并进行监视。
  • 若该属性 【对象类型】属性
    • 可以直接编写属性名进行监视
    • 可以写成函数形式
    • 如果要深度监视对象类型的属性,必须在watch()的选项中设置deep: true
html 复制代码
<template>
  <div>
    <p>姓名: {{ person.name }}</p>
    <p>年龄: {{ person.age }}</p>
    <p>职业:{{ person.details.job }}</p>
    <p>年级:{{ person.details.grade }}</p>
    <button @click="changeName">修改名字</button>
    <button @click="changeJob">修改职业</button>
    <button @click="changeGrade">修改年级</button>
    <button @click="changeDetails">修改详细信息</button>
  </div>
</template>
<script setup lang="ts">
import { reactive, watch } from 'vue';

const person = reactive({
  name: '张三',
  age: 18,
  details: {
    job: 'senior high school student',
    grade: '高三'
  }
});

const changeName = () => {
  person.name += '哈'  
}

const changeJob = () => {
  person.details.job = 'undergraduate'   
}

const changeGrade = () => {
  person.details.grade = '大一'   
}

const changeDetails = () => {
  person.details = {
    job: 'fresh graduate',
    grade: '毕业啦!'
  }
}

// 监视person对象的name属性(基本类型),watch()的第一个参数写成函数式(getter函数)
// () => person.name是一个 getter 函数,它返回person对象的name属性值。
watch(() => person.name, (newName, oldName) => {
  console.log('newName:', newName, 'oldName:', oldName)
});


// 监视details属性中的job属性,watch()的第一个参数写成函数式(getter函数)
// () => person.details.job是一个 getter 函数,它返回person.details.job的值。
watch(() => person.details.job, (newJob, oldJob) => {
  console.log('newJob:', newJob, 'oldJob:', oldJob)
});


// 监视person对象的details属性(对象类型),watch()的第一个参数写成函数式(getter函数)
// () => person.details, 它返回person对象的details属性值。
watch(() => person.details, (newDetails, oldDetails) => {
  console.log('newDetails:', newDetails, 'oldDetails:', oldDetails)
});

// 监视person对象的details属性(对象类型),watch()的第一个参数可以直接写变量
watch(person.details, (newDetails, oldDetails) => {
  console.log('newDetails:', newDetails, 'oldDetails:', oldDetails)
});

// 监视person对象的details属性(对象类型),watch()的第一个参数可以直接写变量
watch(person.details, (newDetails, oldDetails) => {
  console.log('newDetails:', newDetails, 'oldDetails:', oldDetails)
});
</script>

在这个例子中:

  • watch()的第一个参数写成函数式(getter函数)。例如:() => person.name是一个 getter 函数,它返回person对象的name属性值。
  • changeJobchangeGrade不会触发watch(() => person.details, (newDetails, oldDetails) => {}),因为没有开启深度监视。
  • changeDetails 会触发两个监视:
    • watch(() => person.details.job, (newJob, oldJob) => {})
    • watch(() => person.details, (newDetails, oldDetails) => {})

如果要深度监视对象类型的属性,必须在watch()的选项中设置deep: true

例如,改变person.details.jobperson.details.grade 会 触发watch(person.details)

javascript 复制代码
// 监视person对象的details属性(对象类型),写成函数式
watch(() => person.details, (newDetails, oldDetails) => {
  console.log('newDetails:', newDetails, 'oldDetails:', oldDetails)
}, { deep: true });

设置deep: truechangeJobchangeGradechangeDetails都会触发watch(() => person.details, (newDetails, oldDetails) => {})

监视多个响应式数据

html 复制代码
<template>
  <div>
    <p>姓名: {{ name }}</p>
    <p>年龄: {{ age }}</p>
    <button @click="changeName">修改名字</button>
    <button @click="changeAge">修改年龄</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

let name = ref('张三')
let age = ref(18)
const changeName = () => {
  name.value = `${name.value}哈`
}
const changeAge = () => {
  age.value++
}

watch([name, age], (newValue, oldValue) => {
  console.log('newValue', newValue) // newValue是一个数组:[name, age]
  console.log('oldValue', oldValue) // oldValue是一个数组:[name, age]
})

// 当监视多个响应式数据时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
  console.log(`Name changed from ${oldName} to ${newName}`)
  console.log(`Age changed from ${oldAge} to ${newAge}`)
})
</script>

在这个例子中,监视了 nameage 两个响应式数据的变化。当其中任何一个数据发生变化时,回调函数会被执行,打印出两个数据的旧值和新值。

停止监视

使用 watch() 函数创建的监视器,可以通过调用watch()函数的返回值(是一个函数)来停止监视。

html 复制代码
<template>
  <div>
    <div>count: {{ count }}</div>
    <button @click="addCount">点击 count+1</button>
  </div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';

let count = ref(0)

const addCount = () => {
  count.value++
}

// watch 监视的是 ref定义的数据:count
// watch()的返回值是一个函数,调用这个函数可以停止监视。
const stopWatcher = watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`);
  // 当newValue > 10,结束监视
  if(newValue > 10) {
    // 调用stopWatcher ,结束监视
    stopWatcher ()
  }
})
</script>

立即执行回调函数

html 复制代码
<template>
  <div>
    <p>Counter: {{ counter }}</p>
  </div>
</template>

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

const counter = ref(0)

watch(
  counter,(newValue, oldValue) => {
    console.log(`Counter changed from ${oldValue} to ${newValue}`)
  },
  { immediate: true }
)
</script>

通过设置选项对象的immediate: true,可以让watch()在创建后立即执行一次回调函数,无论被监视的数据是否已经发生变化。

此时,oldValue 的值为 undefined

只执行一次的回调函数

html 复制代码
<template>
  <div>
    <p>Counter: {{ counter }}</p>
  </div>
</template>

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

const counter = ref(0)

watch(
  counter,(newValue, oldValue) => {
    console.log(`Counter changed from ${oldValue} to ${newValue}`)
  },
  { once: true }
)
</script>

通过设置选项对象的once: true,回调函数只会执行一次。侦听器将在回调函数首次运行后自动停止。

副作用清除

javascript 复制代码
watch(id, async (newId, oldId, onCleanup) => {
  const { response, cancel } = doAsyncWork(newId)
  // 当 `id` 变化时,`cancel` 将被调用,
  // 取消之前的未完成的请求
  onCleanup(cancel)
  data.value = await response
})

watchEffect()

  • 基本概念
    watchEffect()立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
    • watchEffect()自动追踪函数中使用的响应式数据,并在这些数据发生变化时重新执行函数。
    • watch()不同,watchEffect()不需要明确指定要监视的数据源,它会自动分析函数内部的依赖关系(函数中用到哪些属性,那就监视哪些属性)。
  • 返回值
    • 返回一个函数,调用这个函数可以停止副作用的执行。

watchEffect()参数说明

watchEffect()函数接收以下参数:

  • 回调函数

    • 这是一个必须的参数,它是一个函数,在这个函数内部可以访问响应式数据和执行副作用逻辑。
    • 当使用watchEffect()时,它会立即执行这个回调函数,并在其依赖的响应式数据发生变化时再次执行这个回调函数。
    • 例如:watchEffect(() => { /* 副作用逻辑 */ })
  • 选项对象(可选)

    • flush:控制回调函数的执行时机。参考回调的刷新时机watchEffect()
      • 默认值是'pre',表示在 DOM 更新之前执行回调;
      • 'post'表示在 DOM 更新之后执行回调;
      • 'sync'表示同步执行回调,即立即执行回调函数,并且在响应式数据变化时同步更新视图。
    • onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器
      • onTrack(用于调试):当响应式数据被读取并作为依赖被追踪时,这个函数会被调用。可以在这个函数中记录哪些数据被读取了,以便进行调试和分析依赖关系。
      • onTrigger(用于调试):当响应式数据发生变化并触发依赖时,这个函数会被调用。可以在这个函数中记录哪些数据发生了变化,以便进行调试和分析依赖关系。

示例

基本用法

html 复制代码
<template>
  <div>
    <p>a: {{ a }}</p>
    <p>b: {{ b }}</p>
    <p>sum: {{ sum }}</p>
    <button @click="incrementA">点击 a+2</button>
    <button @click="incrementB">点击 b+5</button>
    <button @click="incrementSum">点击 sum+1</button>
  </div>
</template>

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

const a = ref(5)
const b = ref(10)
const sum = ref(0)

const incrementA = () => {
  console.log("incrementA");
  a.value = a.value + 2
}

const incrementB = () => {
  console.log("incrementB");
  b.value = b.value + 5
}

const incrementSum = () => {
  console.log("incrementSum");
  sum.value = sum.value + 1
}

// watchEffect()的返回值是一个函数,调用这个函数可以停止监视。
let stopEffect = watchEffect(() => {
  console.log('a:', a.value, 'b:', b.value)
  if(a.value < 20 && b.value < 100){
    sum.value = a.value + b.value
  }

  if(a.value > 20 || b.value > 100) {
    stopEffect()
  }
});

watch(sum, (newSum, oldSum) => {
  console.log('newSum', newSum, 'oldSum', oldSum)
})
</script>

在这个例子中:

  • watchEffect()会立即执行传入的函数,并在ab的值发生变化时重新执行该函数,根据条件计算sum的值。
  • watchEffect()的返回值是一个函数,调用这个函数可以停止监视。
  • incrementSum直接修改sum的值,不会触发watchEffect()。因为sumwatchEffect()是作为计算结果被赋值的,而不是直接被读取使用。
  • watch用于监视sum的变化,当sum的值发生变化时,打印出新值和旧值。

使用watchEffect并带有选项对象

javascript 复制代码
watchEffect(() => {
  console.log('a:', a.value, 'b:', b.value)
  if(a.value < 20 && b.value < 100){
    sum.value = a.value + b.value
  }
},
{
  flush: 'post' // 在 DOM 更新之后执行回调 
}
);

在这个例子中,watchEffect()的回调函数会在ab的值发生变化时执行,并在 DOM 更新之后计算sum的值。选项对象中的flush被设置为'post',以控制回调函数的执行时机。

副作用清除

javascript 复制代码
watchEffect(async (onCleanup) => {
  const { response, cancel } = doAsyncWork(id.value)
  // `cancel` 会在 `id` 更改时调用
  // 以便取消之前
  // 未完成的请求
  onCleanup(cancel)
  data.value = await response
})

watch()watchEffect()的区别

  • 数据源
    • watch()需要明确指定要监视的数据源,可以是一个响应式数据、一个返回响应式数据的函数或者一个包含多个响应式数据的数组。
    • watchEffect()不需要明确指定数据源,它会自动追踪函数内部使用的响应式数据。
  • 回调函数参数
    • watch()的回调函数接收两个参数:新值和旧值。如果监视多个数据源,新值和旧值分别是一个包含新数据源值的数组和一个包含旧数据源值的数组。
    • watchEffect()的回调函数不接收新值和旧值参数,它只接收一个用于停止副作用的清理函数作为可选参数。
  • 是否立即执行
    • watch()在创建时不会立即执行回调函数,除非设置immediate: true
    • watchEffect()在创建时会立即执行传入的函数。

watchPostEffect()​

watchEffect() 使用 flush: 'post' 选项时的别名。

watchSyncEffect()​

watchEffect() 使用 flush: 'sync' 选项时的别名。

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm