前端小白系列——搞懂vue中的watch、watchEffect

95 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。 个人工作中涉及到vue的监听机制,之前一直对vue的监听机制只是简单了解,这次对vue的监听机制做了一个全面了解。

1. vue3中的watch

1.1 基本用法

当我们需要在数据变化时执行一些“副作用”:如更改 DOM、执行异步操作,侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数,我们可以使用 watch 函数:

<script setup> 
import { watch } from 'vue'
watch(sources, callback, option)
</script>

1.1 监听属性源(source)

第一个参数是侦听器的源,这个数据源的可以是以下几种:

  • 一个getter(有返回值)函数

对于函数

const getData = () => return data
watch(() => getData, (oldValue, newValue) => {})
  • 一个ref

对于ref定义的响应式数据源

<script setup>
  import {ref, watch} from 'vue'
  const state = ref(0)
  const add = () => {
      state.value++
  }
  watch(state, (newValue, oldValue) => {
      console.log("值改变了:", newValue, oldValue);
  })
</script>

image.png

  • 一个响应式对象(当直接侦听一个响应式对象时,侦听器会自动启用深层模式。)

对于reactive对应的响应式对象

<template>
  <div>
      <div>{{obj.name}}</div>
      <div>{{obj.age}}</div>
      <button @click="changeName">改变值</button>
  </div>
</template>

<script setup>
  import {reactive, watch} from 'vue'
  const obj = reactive({
    name: 'coderzyq',
    age: 25
  })
  const changeName = () => {
    obj.name = 'zyq'
  }
  watch(obj, (newValue, oldValue) => {
    console.log('改变后的值:' + newValue);
    console.log("改变之前的值:" + oldValue);
    /* 注意:在嵌套的变更中, 
       只要没有替换对象本身, 
       那么这里的 `newValue` 和 `oldValue` 相同*/
  })
</script>

image.png

对于此监听数据源出现的问题,在下文的配置选项深度监听会进行解决。

对于监听响应式对象(props、computed、reactive和ref等)的某个属性值

<template>
  <div>
    <div>{{ obj.name }}</div>
    <div>{{ obj.age }}</div>
    <button @click="changeName">改变值</button>
  </div>
</template>

<script setup>
import { reactive, watch } from "vue";
const obj = reactive({
  name: "coderzyq",
  age: 25,
});
const changeName = () => {
  obj.name = "zyq";
};
watch(
  () => obj.name,
  (newValue, oldValue) => {
    console.log("改变后的值:" + newValue);
    console.log("改变之前的值:" + oldValue);
  }
);
</script>

image.png

(注意,你不能直接侦听响应式对象的属性值,例如:)

<script setup>
import { reactive, watch } from "vue";
const obj = reactive({ name: "coderzyq" })

// 错误,因为 watch() 得到的参数是一个 number
watch(obj.name, (value) => {
  console.log(`count is: ${value}`)
})
</script>

image.png

要需要用一个返回该属性的 getter 函数

//要提供一个getter函数
watch(
  () => obj.name, 
  (value) => {
  console.log(`count is: ${value}`)
})
  • 或者由以上类型的值组成的数组
<template>
  <div>
    <div>{{ obj.name }}</div>
    <div>{{ obj.age }}</div>
    <div>{{ state }}</div>
    <button @click="change">改变值</button>
  </div>
</template>

<script setup>
import { reactive, ref, watch } from "vue";
const state = ref("state")
const obj = reactive({
  name: "coderzyq",
  age: 25,
});
const change = () => {
  obj.name = "zyq";
};
watch(
  [state, obj],
  () => {
    console.log('监听了多个数据源');
  }
);
</script>

image.png

1.2 回调函数(callback(newValue, oldValue, onCleanup))

第二个参数是数据源发生变化要调用的回调函数,这个函数有三个参数:旧值、新值以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。当侦听多个数据源时,回调函数接收两个数组,分别对应数据源数组中的旧值和新值。

callback(newValue, oldValue, onCleanup)
//newValue 和 oldValue是一个对象
//onCleanup参数

watch的返回值是一个用来停止该副作用的函数

const stop = watch(() => {})
//中止副函数
stop()

注意,使用同步语句创建的侦听器,会自动绑定到当前组件实例上,并且会在当前组件卸载时自动停止。对于异步创建的侦听器,则不会绑定到当前组件上,因此要手动停止它,以防内存泄漏,如下所示:

//组件卸载会自动停止
  watch(
    obj,
    () => {}
  )
  //必须手动卸载, 组件卸载时不会停止
  let timer = setTimeout(() => {
    watch(
    obj,
    () => {}
  )
  }, 100)
  clearTimeout(timer)

1.3 配置选项(option)

第三个参数是依赖项,是一个对象

  • immediate:在侦听器创建时立即触发回调。

watch() 是懒执行的:当数据源发生变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。

const url = ref('url')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// 立即执行一次,再侦听 url 变化
watch(url, fetchData, { immediate: true })
  • deep:深度遍历,以便在深层级变更时触发回调。

当watch监听一个响应式对象时,会默认创建一个深层侦听器,所有的嵌套的属性变更时都会被触发。 但一个返回响应式对象的getter函数,只有在对象被替换时才会触发

watch(
  () => obj.someObject,
  () => {
    // 仅当 obj.someObject 被替换时触发
  }
)

你也可以显式地加上 deep 选项,强制转成深层侦听器

watch(
  () => obj.someObject,
  (newValue, oldValue) => {
    // `newValue` 此处和 `oldValue` 是相等的
    // 除非 obj.someObject 被整个替换了
    console.log('deep', newValue.count, oldValue.count)
  },
  { deep: true }
)

obj.someObject.count++ // deep 1 1

对于深层侦听一个响应式对象或者数组,新值和旧值是相等的问题,我们可通过对值进行深拷贝的形式进行侦听。

watch(
  () => _.cloneDeep(obj.someObject),
  (newValue, oldValue) => {
    // 此时 `newValue` 此处和 `oldValue` 是不相等的
    console.log('deep', newValue.count, oldValue.count)
  },
  { deep: true }
)

obj.someObject.count++ // deep 1 0

注:深层侦听需要遍历所有嵌套的属性,当数据结构庞大时,开销很大,因此要谨慎使用,以此来提高性能。

  • flush:回调函数的触发时机。pre:默认,dom 更新前调用,post: dom 更新后调用,sync 同步调用。

默认情况下,创建的侦听器回调,都会在vue组件更新之前被调用,这意味这我们侦听到的DOM是在vue更新之前的状态,要获取到最新的DOM,就要配置 flush: 'post',类似于nextick()。

<template>
  <div>
    <div id="num">{{ count }}</div>
    <button @click="change">改变值</button>
  </div>
</template>
<script setup>
import { ref, watch } from "vue";
const count = ref(0);
const change = () => {
  count.value++;
};
watch(
  count, 
  () => {
    console.log(count.value);
    console.log(document.querySelector("#num").innerText);
  }
);
</script>

打印结果

image.png 由上图可以看出对于DOM中的数值和更新之后的数值并不相等,因此想要在侦听器中能访问到Vue更新之后的DOM,要配置flush: 'post'配置项。

watch(count, 
  () => {
    console.log(count.value);
    console.log(document.querySelector("#num").innerText);
  },
  {flush: 'post'}
  )

上述写法也可写成

import { watchPostEffect } from 'vue' 
watchPostEffect(() => { /* 在 Vue 更新后执行 */ })

image.png

onTrack / onTrigger:用于调试的钩子。在依赖收集和回调函数触发时被调用。

onTrack 和 onTrigger 选项可用于调试侦听器的行为。 这两个回调都将接收到一个包含有关所依赖项信息的调试器事件

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。
watch(
    source, 
    callback, 
    { 
    onTrack(e) { debugger }, 
    onTrigger(e) { debugger }
    }
)

侦听器的 onTrack 和 onTrigger 选项仅会在开发模式下工作

2. vue3中的watchEffect

watchEffectwatch进行简化,接收两个参数,第一个参数是数据发生变化时执行的回调函数,第二个参数是一个可选的对象(依赖项)。

2.1 回调函数(callback)

watchEffect 会立即执行一遍回调函数,如果这时函数产生了副作用,Vue 会自动追踪副作用的依赖关系,自动分析出侦听数据源。写法如下:

const url = ref('url地址') 
const data = ref(null) 
// 一个参数就可以搞定 
watch(
    callback,
    option
)
watchEffect(async () => { 
    const response = await fetch(url.value) 
    data.value = await response.json() 
  }
)

注:当使用异步回调时,只有在第一个await之前的访问到的依赖才会进行追踪。

2.2 依赖项(option)

功能和watch相同 注意:watchEffect

3. watch和watchEffect

watch 和 watchEffect 的主要功能是相同的,都能响应式地执行有副作用的回调。它们的区别是追踪响应式依赖的方式不同:

3.1 watch

  • watch 只追踪明确定义的数据源,不会追踪在回调中访问到的东西;默认情况下,只有在数据源发生改变时才会触发回调;
  • watch 可以访问侦听数据的新值和旧值,
  • watch会避免发生副作用时追踪依赖,因此能够准确地控制回调函数地触发时机。

3.2 watchEffect

  • watchEffect 会初始化执行一次,在副作用发生期间追踪依赖,自动分析出侦听数据源,代码会更加简洁,可以清除手动维护依赖列表地的负担,
  • watchEffect仅会在同步期间,才追踪依赖。当使用异步回调时,只有在第一个await之前的访问到的依赖才会进行追踪。
  • watchEffect可能会比watch的深度侦听器更加有效,因此它将只跟踪回调中被使用的属性,而不是递归地跟踪所有地属性
  • watchEffect 无法访问侦听数据的新值和旧值,有时其响应式关系不会那么明确。