Vue 3源码深度解析之:`watch`和`watchEffect`:它们的执行时机、参数差异与应用场景。

大家好,我是你们的老朋友,今天咱们来聊聊Vue 3里一对形影不离的好兄弟:watchwatchEffect。 别看它们名字有点像,功能也都是监听数据变化,但内部的运行机制和使用场景可是大有不同。 搞清楚这些差异,能让你在Vue的世界里更加游刃有余,写出更高效、更优雅的代码。

开场白:侦察兵与预言家

如果把数据变化比作战场上的风吹草动,watch就像一位经验丰富的侦察兵,你需要明确告诉他要监视哪个目标,他才会时刻关注,并汇报目标的最新动向。 而watchEffect则更像一位拥有预知能力的预言家,他会主动感知周围环境的变化,并根据这些变化做出相应的反应,无需你明确指定监视目标。

第一部分:watch – 精准打击的侦察兵

watch 允许你监听一个或多个数据源,并在数据源发生变化时执行回调函数。 它可以监听的类型非常广泛:

  • 响应式数据: refreactive 创建的数据。
  • getter 函数: 返回值的函数。
  • 多个数据源: 以数组形式传入。

1. 基本用法:

import { ref, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);

    // 监听 count 的变化
    watch(count, (newValue, oldValue) => {
      console.log(`count 从 ${oldValue} 变成了 ${newValue}`);
    });

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment
    };
  }
};

在这个例子中,我们使用 watch 监听了 count 这个 ref 变量的变化。 当 count 的值发生改变时,回调函数就会被执行,并打印出新旧值。

2. 监听多个数据源:

import { ref, reactive, watch } from 'vue';

export default {
  setup() {
    const firstName = ref('张');
    const lastName = ref('三');
    const fullName = ref('');

    // 监听 firstName 和 lastName 的变化
    watch([firstName, lastName], ([newFirstName, newLastName], [oldFirstName, oldLastName]) => {
      fullName.value = newFirstName + newLastName;
      console.log(`姓名从 ${oldFirstName + oldLastName} 变成了 ${newFirstName + newLastName}`);
    }, { immediate: true }); // 立即执行一次

    return {
      firstName,
      lastName,
      fullName
    };
  }
};

这里,我们使用数组形式传入了 firstNamelastName 两个 ref 变量。 当其中任何一个变量发生变化时,回调函数都会被执行。 回调函数的第一个参数是一个数组,包含了新的 firstNamelastName 的值;第二个参数也是一个数组,包含了旧的 firstNamelastName 的值。

3. 监听 reactive 对象中的属性:

import { reactive, watch } from 'vue';

export default {
  setup() {
    const person = reactive({
      name: '李四',
      age: 20
    });

    // 监听 person.age 的变化
    watch(() => person.age, (newValue, oldValue) => {
      console.log(`年龄从 ${oldValue} 变成了 ${newValue}`);
    });

    const growUp = () => {
      person.age++;
    };

    return {
      person,
      growUp
    };
  }
};

注意,这里我们使用了一个 getter 函数 () => person.age 来监听 person 对象的 age 属性。 直接传入 person.age 是不行的! 因为这样传递的是一个原始值,而不是响应式引用,watch无法追踪到变化。 getter 函数确保了我们监听的是响应式依赖。

4. watch 的选项:

watch 还可以接受一个可选的配置对象,用于控制其行为:

选项 类型 描述
immediate boolean 是否在组件挂载后立即执行回调函数。 默认值为 false
deep boolean 是否深度监听对象内部的变化。 默认值为 false
flush 'pre' | 'post' | 'sync' 指定回调函数的刷新时机。 pre (默认): 在组件更新之前执行。 post: 在组件更新之后执行。 sync:同步执行。 (通常不建议使用sync,可能导致性能问题)
onTrack Function 用于调试侦听器的依赖项跟踪。
onTrigger Function 用于调试侦听器的依赖项触发。
flush: 'sync' string 同步的刷新时机。 极少使用, 可能会阻塞UI。
  • immediate: truewatch 在组件初始化时立即执行一次回调函数。 这在某些需要根据初始值进行一些操作的场景下非常有用。

  • deep: true 用于深度监听对象内部的变化。 默认情况下,watch 只会监听对象的引用是否发生变化,而不会监听对象内部属性的变化。 如果需要监听对象内部属性的变化,就需要将 deep 设置为 true请谨慎使用 deep: true,因为它会带来性能上的开销。

import { reactive, watch } from 'vue';

export default {
  setup() {
    const person = reactive({
      address: {
        city: '北京'
      }
    });

    // 深度监听 person.address.city 的变化
    watch(() => person.address.city, (newValue, oldValue) => {
      console.log(`城市从 ${oldValue} 变成了 ${newValue}`);
    }, { deep: true }); // 加上 deep: true 才能监听到

    const changeCity = () => {
      person.address.city = '上海';
    };

    return {
      person,
      changeCity
    };
  }
};

在这个例子中,我们使用了 deep: true 来深度监听 person.address.city 的变化。 如果没有 deep: truewatch 将不会监听到 city 的变化。

  • flush: 'post' 默认情况下,watch 的回调函数会在组件更新之前执行。 如果你需要在组件更新之后执行回调函数,可以将 flush 设置为 'post'。 这在某些需要访问更新后的 DOM 的场景下非常有用。

第二部分:watchEffect – 自我感知的预言家

watchEffect 会立即执行一次回调函数,并在回调函数执行过程中自动追踪所有响应式依赖。 当这些依赖发生变化时,回调函数会自动重新执行。

1. 基本用法:

import { ref, watchEffect } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watchEffect(() => {
      console.log(`count 的值为:${count.value}`);
    });

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment
    };
  }
};

在这个例子中,watchEffect 会立即执行一次回调函数,并在回调函数执行过程中追踪到 count 这个 ref 变量。 当 count 的值发生改变时,回调函数会自动重新执行。 注意,watchEffect 不需要你手动指定监听哪个数据源,它会自动追踪回调函数中用到的所有响应式数据。

2. 停止监听:

watchEffect 会返回一个停止函数,你可以调用这个函数来停止监听。

import { ref, watchEffect, onUnmounted } from 'vue';

export default {
  setup() {
    const count = ref(0);

    const stop = watchEffect(() => {
      console.log(`count 的值为:${count.value}`);
    });

    const increment = () => {
      count.value++;
    };

    // 在组件卸载时停止监听
    onUnmounted(() => {
      stop();
    });

    return {
      count,
      increment
    };
  }
};

在这个例子中,我们使用 onUnmounted 生命周期钩子函数在组件卸载时调用 stop 函数来停止监听。 记住,一定要在组件卸载时停止监听,否则可能会导致内存泄漏。

3. watchEffect 的选项:

watchEffect 也可以接受一个可选的配置对象,用于控制其行为:

选项 类型 描述
flush 'pre' | 'post' | 'sync' 指定回调函数的刷新时机。 pre (默认): 在组件更新之前执行。 post: 在组件更新之后执行。 sync:同步执行。 (通常不建议使用sync,可能导致性能问题)
onTrack Function 用于调试侦听器的依赖项跟踪。
onTrigger Function 用于调试侦听器的依赖项触发。

flushonTrackonTrigger 的作用与 watch 中的相同。

4.清除副作用

watchEffect 的回调函数可以返回一个清除函数。这个清除函数会在下次回调函数执行之前被调用。这对于清除副作用非常有用,例如取消定时器、移除事件监听器等。

import { ref, watchEffect, onUnmounted } from 'vue';

export default {
  setup() {
    const count = ref(0);

    watchEffect((onInvalidate) => {
      const timer = setTimeout(() => {
        console.log(`count 的值为:${count.value}`);
      }, 1000);

      // 在下次回调函数执行之前清除定时器
      onInvalidate(() => {
        clearTimeout(timer);
      });
    });

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment
    };
  }
};

在这个例子中,我们在 watchEffect 的回调函数中设置了一个定时器。 为了避免定时器重复执行,我们在 onInvalidate 函数中清除了定时器。 onInvalidate 函数会在下次回调函数执行之前被调用。

第三部分:watch vs watchEffect – 侦察兵与预言家的选择

特性 watch watchEffect
监听目标 需要明确指定要监听的数据源。 自动追踪回调函数中用到的所有响应式数据。
执行时机 只有在监听的数据源发生变化时才会执行回调函数。 立即执行一次回调函数,并在依赖发生变化时自动重新执行。
依赖追踪 手动指定依赖。 自动追踪依赖。
停止监听 返回值不是停止函数,需要手动管理停止。 返回一个停止函数,用于停止监听。
清除副作用 依赖变化后才执行回调。 如果需要清除副作用,需要在回调函数内部手动处理。 可返回清除函数,在下次回调函数执行之前执行,用于清除副作用。
使用场景 需要精确控制监听目标,并且只需要在特定数据源发生变化时才执行回调函数的场景。 需要根据多个数据源的变化来执行一些操作,并且希望自动追踪依赖的场景。
适用场景举例 1. 监听单个或多个特定属性的变化,例如监听用户名的变化并更新欢迎信息。 2. 监听路由的变化并加载不同的组件。 3. 基于一些条件判断是否执行副作用。 1. 根据多个响应式数据计算出一个新的值。 2. 在组件初始化时执行一些副作用,例如发送网络请求、设置定时器等。 3. 根据多个响应式数据的变化来更新 DOM。
性能考量 由于需要手动指定监听目标,因此可以更精确地控制监听范围,避免不必要的性能开销。 由于会自动追踪依赖,因此可能会追踪到一些不必要的依赖,导致性能开销增加。
初次执行 只有当监听的属性发生变化时才会执行回调函数(除非设置了immediate: true)。 会在组件挂载后立即执行一次回调函数。
显式/隐式依赖 依赖关系是显式声明的,通过传递 refreactive 属性给 watch 依赖关系是隐式的,通过在 watchEffect 回调函数中访问响应式属性来建立。

形象的比喻:

  • watch 就像一个订阅服务,你需要告诉它你对哪些信息感兴趣,它才会给你发送相关的通知。
  • watchEffect 就像一个智能家居系统,它会自动感知房间里的温度、湿度、光线等变化,并根据这些变化自动调节空调、加湿器、灯光等设备。

总结:

watchwatchEffect 都是 Vue 3 中非常重要的 API,它们可以帮助你监听数据变化并执行相应的操作。 选择哪个 API 取决于你的具体需求。 如果你需要精确控制监听目标,并且只需要在特定数据源发生变化时才执行回调函数,那么 watch 是一个不错的选择。 如果你需要根据多个数据源的变化来执行一些操作,并且希望自动追踪依赖,那么 watchEffect 可能会更方便。 记住,没有绝对的好与坏,只有适合与不适合。

第四部分:代码示例:更贴近实战

场景 1:监听用户输入,进行搜索

<template>
  <input type="text" v-model="searchText">
  <ul>
    <li v-for="item in searchResults" :key="item">{{ item }}</li>
  </ul>
</template>

<script>
import { ref, watch } from 'vue';

export default {
  setup() {
    const searchText = ref('');
    const searchResults = ref([]);

    // 模拟搜索函数
    const performSearch = async (text) => {
      // 模拟网络请求
      await new Promise(resolve => setTimeout(resolve, 500));
      // 模拟搜索结果
      return Array.from({ length: 5 }, (_, i) => `${text}-Result-${i + 1}`);
    };

    // 监听 searchText 的变化
    watch(searchText, async (newText) => {
      if (newText.length > 0) {
        searchResults.value = await performSearch(newText);
      } else {
        searchResults.value = [];
      }
    });

    return {
      searchText,
      searchResults
    };
  }
};
</script>

在这个例子中,我们使用 watch 监听了 searchText 的变化。 当用户输入时,searchText 的值会发生改变,watch 的回调函数会被执行,并调用 performSearch 函数进行搜索。

场景 2:根据窗口大小调整布局

<template>
  <div>
    <p>当前布局:{{ layout }}</p>
  </div>
</template>

<script>
import { ref, watchEffect } from 'vue';

export default {
  setup() {
    const layout = ref('desktop');

    watchEffect(() => {
      if (window.innerWidth < 768) {
        layout.value = 'mobile';
      } else {
        layout.value = 'desktop';
      }
    });

    return {
      layout
    };
  }
};
</script>

在这个例子中,我们使用 watchEffect 监听了窗口大小的变化。 当窗口大小发生改变时,watchEffect 的回调函数会被执行,并根据窗口大小调整 layout 的值。

场景 3:根据用户角色显示不同的权限

<template>
  <div>
    <p v-if="hasAdminPermission">管理员权限</p>
    <p v-if="hasEditorPermission">编辑权限</p>
    <p v-if="hasViewerPermission">查看权限</p>
  </div>
</template>

<script>
import { ref, reactive, computed, watchEffect } from 'vue';

export default {
  setup() {
    const user = reactive({
      role: 'viewer' // 或者 'editor', 'admin'
    });

    const hasAdminPermission = computed(() => user.role === 'admin');
    const hasEditorPermission = computed(() => user.role === 'editor' || user.role === 'admin');
    const hasViewerPermission = computed(() => user.role === 'viewer' || user.role === 'editor' || user.role === 'admin');

    // 模拟用户角色变化
    const changeRole = (newRole) => {
      user.role = newRole;
    };

    return {
      hasAdminPermission,
      hasEditorPermission,
      hasViewerPermission,
      changeRole
    };
  }
};
</script>

在这个例子中,我们使用了computed来计算用户权限,并且使用了watchEffect来响应用户角色变化。虽然这里用watchEffect也能实现,但更好的方式是使用computed,因为权限本质上是基于用户角色的派生数据,computed更适合描述这种关系。 如果用watchEffect,每当user.role变化,watchEffect会重新运行,虽然也能更新权限,但不如computed高效和语义化。

总结:

希望通过今天的讲解,大家对 Vue 3 中的 watchwatchEffect 有了更深入的了解。 在实际开发中,要根据具体场景选择合适的 API,并注意性能优化,写出更高效、更优雅的代码。 记住,熟练掌握这些基础知识,才能在 Vue 的世界里自由驰骋!

今天的讲座就到这里,谢谢大家! 祝大家编程愉快,Bug 少少!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注