Skip to content

侦听器 watchEffect

watchEffect

watchEffect 相比 watch 而言,能够自动跟踪回调里面的响应式依赖,对比如下:

watch

js
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 }
);

watchEffect

js
// 不再需要显式的指定响应式数据依赖
// 在回调函数中用到了哪个响应式数据,该数据就会成为一个依赖
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  );
  data.value = await response.json();
});

对于只有一个依赖项的场景来讲,watchEffect 的收益不大,但是如果涉及到多个依赖项,那么 watchEffect 的好处就体现出来了。

watchEffect 相比 watch 还有一个特点:

  • 如果你需要侦听一个嵌套的数据结构的几个属性,那么 watchEffect 只会侦听回调中用到的属性,而不是递归侦听所有的属性。
  • watchEffect 会立即执行一次
vue
<template>
  <div>
    <h1>团队管理</h1>
    <ul>
      <li v-for="member in team.members" :key="member.id">
        {{ member.name }} - {{ member.role }} - {{ member.status }}
      </li>
    </ul>
    <button @click="updateLeaderStatus">切换领导的状态</button>
    <button @click="updateMemberStatus">切换成员的状态</button>
  </div>
</template>

<script setup>
import { reactive, watchEffect } from "vue";
const team = reactive({
  members: [
    { id: 1, name: "Alice", role: "Leader", status: "Active" },
    { id: 2, name: "Bob", role: "Member", status: "Inactive" },
  ],
});

// 有两个方法,分别是对 Leader 和 Member 进行状态修改
function updateLeaderStatus() {
  const leader = team.members.find((me) => me.role === "Leader");
  // 切换状态
  leader.status = leader.status === "Active" ? "Inactive" : "Active";
}

function updateMemberStatus() {
  const member = team.members.find((member) => member.role === "Member");
  member.status = member.status === "Active" ? "Inactive" : "Active";
}

// 添加一个侦听器
watchEffect(() => {
  // 获取到 leader
  //只会监听leader那一条数据 { id: 1, name: 'Alice', role: 'Leader', status: 'Active' },
  //改变Member不会触发
  const leader = team.members.find((me) => me.role === "Leader");
  // 输出 leader 当前的状态
  console.log("Leader状态:", leader.status);
});
</script>

<style scoped></style>

回调触发的时机

默认情况下,侦听器回调的执行时机在父组件更新 之后,所属组件的 DOM 更新 之前 被调用。这意味着如果你尝试在回调函数中访问所属组件的 DOM,拿到的是 DOM 更新之前的状态

vue
<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <div v-if="isShow" ref="divRef">
      <p>this is a test</p>
    </div>
    <p>上面的高度为:{{ height }} pixels</p>
  </div>
</template>

<script setup>
import { ref, watch } from "vue";
const isShow = ref(false);
const height = ref(0); // 存储高度
const divRef = ref(null); // 获取元素

watch(isShow, () => {
  // 获取高度,将高度值给 height
  height.value = divRef.value ? divRef.value.offsetHeight : 0;
  console.log(`当前获取的高度为:${height.value}`);
});
</script>

<style scoped></style>

如果我们期望侦听器的回调在 DOM 更新之后再被调用,那么可以将第三个参数 flush 设置为 post 即可,如下:

js
watch(
  isShow,
  () => {
    // 获取高度,将高度值给 height
    height.value = divRef.value ? divRef.value.offsetHeight : 0;
    console.log(`当前获取的高度为:${height.value}`);
  },
  {
    flush: "post", //将执行侦听器的时间放置在dom更新之后
  }
);

停止侦听器

大多数情况下你是不需要关心如何停止侦听器,组件上面所设置的侦听器会在组件被卸载的时候自动停止。

但是上面所说的自动停止仅限于同步设置侦听器的情况,如果是异步设置的侦听器,那么组件被销毁也不会自动停止:

vue
<script setup>
import { watchEffect } from "vue";

// 它会自动停止
watchEffect(() => {});

// ...这个则不会!
setTimeout(() => {
  watchEffect(() => {});
}, 100);
</script>

这种情况下,就需要手动的去停止侦听器。

要手动的停止侦听器,就和 setTimeout 或者 setInterval 类似,调用一下返回的函数即可。

js
const unwatch = watchEffect(() => {});
// 手动停止
unwatch();

下面是一个具体的示例:

vue
<template>
  <div>
    <button @click="a++">+1</button>
    <p>当前 a 的值为:{{ a }}</p>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
import { ref, watch } from "vue";
const a = ref(1); // 计数器
const message = ref(""); // 消息
// 假设我们期望 a 的值到达一定的值之后,停止侦听
const unwatch = watch(
  a,
  (newVal) => {
    // 当值大于 5 的时候,停止侦听
    if (newVal > 5) {
      unwatch();
    }
    message.value = `当前 a 的值为:${a.value}`;
  },
  { immediate: true }
);
</script>

<style scoped></style>

-EOF-

MIT License