vue3中的监听

416 阅读6分钟

watch

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数

数据源

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

也可以监听一个计算属性

const newMessage = computed(() => {
  return message.value;
});
watch(newMessage, (newValue, oldValue) => {
  console.log("新的值:", newValue);
  console.log("旧的值:", oldValue);
});

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

const obj = reactive({ count: 0 })

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

这里需要用一个返回该属性的 getter 函数: 这里的 getter 函数可以简单的理解为获取数据的一个函数,说白了该函数就是一个返回值的操作,有点类似于计算属性。

// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)

监听响应式对象

const number = reactive({ count: 0 });
const countAdd = () => {
  number.count++;
};
watch(number, (newValue, oldValue) => {
  console.log("新的值:", newValue);
  console.log("旧的值:", oldValue);
});

当 watch 监听的是一个响应式对象时,会隐式地创建一个深层侦听器,即该响应式对象里面的任何属性发生变化,都会触发监听函数中的回调函数。

vue官方文档中描述为:直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发

**注意深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

但是,如果我们是使用的 getter 函数返回响应式对象的形式,那么响应式对象的属性值发生变化,是不会触发 watch 的回调函数的。

const number = reactive({ count: 0 });
const countAdd = () => {
  number.count++;
};
watch(
  () => number,
  (newValue, oldValue) => {
    console.log("新的值:", newValue);
    console.log("旧的值:", oldValue);
  },
  //{deep: true}
);

上段代码中我们使用 getter 函数返回了响应式对象,当我们更改 number 中 count 的值时,watch 的回调函数是不会执行的。

为了实现上述代码的监听,我们可以手动给监听器加上深度监听的效果。{ deep: true }

注意: 上段代码中的 newValue 和 oldValue 的值是一样的,除非我们把响应式对象即 number 整个替换掉,那么这两个值才会变得不一样。除此之外,深度监听会遍历响应式对象的所有属性,开销较大,当对象体很大时,需要慎用

所以我们推荐 getter 函数只返回相应是对象中的某一个属性!!

监听多个来源的数组

watch 还可以监听数组,前提是这个数组内部含有响应式数据。

const x1 = ref(12);
const number = reactive({ count: 0 });
const countAdd = () => {
  number.count++;
};
watch([x1, () => number.count], (newValue, oldValue) => {
  console.log("新的值:", newValue);
  console.log("旧的值:", oldValue);
});

watchEffect 即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。比如有些场景下我们可能需要刚进页面,或者说第一次渲染页面的时候,watch 监听器里面的回调函数就执行一遍。

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

watch(source, (newValue, oldValue) => { 
// 立即执行,且当 `source` 改变时再次执行
}, { immediate: true })

侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId 的引用发生变化时使用侦听器来加载一个远程资源:

const todoId = ref(1)
const data = ref(null)

watch(todoId, async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
}, { immediate: true })

特别是注意侦听器是如何两次使用 todoId 的,一次是作为源,另一次是在回调中。

我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:

watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})

这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

对于这种只有一个依赖项的例子来说,watchEffect() 的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect() 可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。

watch 和 watchEffect 区别

  • watch 和 watchEffect 都能监听响应式数据的变化,不同的是它们监听数据变化的方式不同。
  • watch 会明确监听某一个响应数据,而 watchEffect 则是隐式的监听回调函数中响应数据。
  • watch 在响应数据初始化时是不会执行回调函数的,watchEffect 在响应数据初始化时就会立即执行回调函数。

回调中的 DOM

在监听器的回调函数里面获取到的 DOM 元素是更新前的

解决方法:

如果我们想要在回调函数里面获取更新后的 DOM,非常简单,我们只需要再给监听器多传递一个参数选项即可:flush: 'post'。watch 和 watchEffect 同理。

watch(source, callback, {
  flush: 'post'
})
watchEffect(callback, {
  flush: 'post'
})

修改后的代码:

watch(
  message,
  (newValue, oldValue) => {
    console.log("DOM 节点", msgRef.value.innerHTML);
    console.log("新的值:", newValue);
    console.log("旧的值:", oldValue);
  },
  {
    flush: "post",
  }
);

这个时候我们在回调函数中获取到的已经是更新后的 DOM 节点了。

虽然 watch 和 watchEffect 都可以用上述方法解决 DOM 问题,但是 Vue3 单独给 watchEffect 提供了一个更方便的方法,也可以叫做 watchEffect 的别名,代码如下:

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

手动停止监听器

通常来说,我们的一个组件被销毁或者卸载后,监听器也会跟着被停止,并不需要我们手动去关闭监听器。但是总是有一些特殊情况,即使组件卸载了,但是监听器依然存在,这个时候其实式需要我们手动关闭它的,否则容易造成内存泄漏。

比如下面这中写法,我们就需要手动停止监听器:

<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

上段代码中我们采用异步的方式创建了一个监听器,这个时候监听器没有与当前组件绑定,所以即使组件销毁了,监听器依然存在。

关闭方法很简单,代码如下:

const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()

我们需要用一个变量接收监听器函数的返回值,其实就是返回的一个函数,然后我们调用该函数,即可关闭当前监听器。

文章学习来源: