Vue3的watch是如何实现数据监听的

605 阅读9分钟

Vue 3 的 watch 如何实现数据监听、优缺点及详细代码讲解

在 Vue 3 中,watch 是一个非常重要的响应式 API,用于响应式地监听数据的变化,并在变化时执行相应的回调函数。本文将深入探讨 Vue 3 的 watch 是如何实现数据监听的,分析其优缺点,并通过详尽的代码示例帮助大佬们全面理解和应用这一功能。

什么是 watch

watch 是 Vue 3 提供的一个响应式 API,用于观察一个或多个响应式数据源(如 refreactive 对象)的变化,并在变化时执行一个回调函数。它类似于 Vue 2 中的 watch 选项,但在 Composition API 中得到了更广泛的应用和增强。

为什么需要 watch

在 Vue 的响应式系统中,computed 通常用于基于响应式数据生成新的派生数据。然而,当需要在数据变化时执行一些副作用(如异步操作、手动 DOM 操作、调用 API 等)时,computed 不再适用。这时,watch 就成为了一个理想的选择。

watch 的内部实现原理

要理解 watch 的实现机制,必须先了解 Vue 3 的响应式系统以及依赖收集与触发的基本原理。

响应式系统概述

Vue 3 的响应式系统基于 ES6 的 Proxy 实现,能够高效地追踪数据的变化。核心概念包括:

  • 响应式对象:通过 reactiveref 创建的对象,实现了数据的自动追踪。
  • 依赖收集:当组件渲染或函数执行时,系统会收集访问的数据作为依赖。
  • 依赖触发:当数据变化时,系统会触发相应的依赖,更新视图或执行回调。

依赖收集与触发

在 Vue 3 中,依赖收集和触发由一个全局的 effect 函数栈管理。基本流程如下:

  1. 依赖收集:当响应式数据被访问时,将当前的 effect 函数添加到该数据的依赖列表中。
  2. 依赖触发:当响应式数据变化时,遍历其依赖列表,重新执行这些 effect 函数。

watch 的实现机制

watch 的实现机制基于 Vue 3 的响应式系统和 effect 函数。其基本流程如下:

  1. 初始化watch 接收一个或多个数据源和一个回调函数。
  2. 创建 Effect:为回调函数创建一个新的 effect,并在执行时追踪数据源的依赖。
  3. 触发回调:当数据源变化时,effect 被重新执行,调用回调函数并传递新值和旧值。

watch 的使用方法

watch 在 Vue 3 的 Composition API 中被广泛使用,以下是其主要的使用方法和选项。

基本用法

import { ref, watch } from 'vue';

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

    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`);
    });

    return { count };
  },
};

监听多个来源

import { ref, watch } from 'vue';

export default {
  setup() {
    const firstName = ref('John');
    const lastName = ref('Doe');

    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
    });

    return { firstName, lastName };
  },
};

深度监听

import { reactive, watch } from 'vue';

export default {
  setup() {
    const user = reactive({
      name: 'John',
      address: {
        city: 'New York',
      },
    });

    watch(
      () => user,
      (newUser, oldUser) => {
        console.log('User object changed:', newUser);
      },
      { deep: true }
    );

    return { user };
  },
};

立即执行

import { ref, watch } from 'vue';

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

    watch(
      count,
      (newVal, oldVal) => {
        console.log(`count changed from ${oldVal} to ${newVal}`);
      },
      { immediate: true }
    );

    return { count };
  },
};

清理副作用

import { ref, watch } from 'vue';

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

    watch(count, (newVal, oldVal, onCleanup) => {
      const timer = setInterval(() => {
        console.log('Count:', count.value);
      }, 1000);

      onCleanup(() => {
        clearInterval(timer);
      });
    });

    return { count };
  },
};

watch 的优缺点

优点

  1. 副作用管理watch 能够在数据变化时执行副作用,如异步操作、手动 DOM 操作等。
  2. 灵活性高:支持监听单个或多个数据源,可以进行深度监听和立即执行等配置。
  3. 组合性强:与 Composition API 无缝集成,便于逻辑复用和代码组织。
  4. 类型支持:在 TypeScript 中,watch 提供了良好的类型推导支持。

缺点

  1. 复杂性:相比 computedwatch 需要更多的配置选项,可能增加代码复杂度。
  2. 性能考虑:深度监听和监听多个数据源时,可能带来一定的性能开销。
  3. 依赖管理:不当使用 watch 可能导致依赖关系混乱,影响代码的可维护性。

详细代码讲解

为了更好地理解 watch 的工作原理和使用方法,以下将通过多个详细的代码示例进行说明。

示例 1:简单的 watch 使用

文件结构

watch-demo/
├── index.html
├── App.vue
└── main.js

index.html

<!-- watch-demo/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vue 3 Watch 示例 - 简单使用</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="./main.js"></script>
</body>
</html>

main.js

// watch-demo/main.js
import { createApp } from 'vue';
import App from './App.vue';

createApp(App).mount('#app');

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 简单使用</h1>
    <p>当前计数: {{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>

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

const count = ref(0);

// 定义一个watch监听count的变化
watch(count, (newVal, oldVal) => {
  console.log(`count从${oldVal}变为${newVal}`);
});

// 增加计数的方法
const increment = () => {
  count.value++;
};
</script>

<style scoped>
h1 {
  color: #42b983;
}
button {
  padding: 10px 20px;
  font-size: 16px;
}
</style>

运行效果

点击“增加计数”按钮,count 的值会增加,并在控制台中打印出变化信息。

示例 2:监听多个数据来源

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 监听多个数据来源</h1>
    <div>
      <label>名字: </label>
      <input v-model="firstName" placeholder="First Name" />
    </div>
    <div>
      <label>姓氏: </label>
      <input v-model="lastName" placeholder="Last Name" />
    </div>
    <p>全名: {{ fullName }}</p>
  </div>
</template>

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

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// 监听多个数据来源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`名字从${oldFirst} ${oldLast}变为${newFirst} ${newLast}`);
});
</script>

<style scoped>
h1 {
  color: #42b983;
}
div {
  margin-bottom: 10px;
}
label {
  margin-right: 10px;
}
input {
  padding: 5px;
  font-size: 14px;
}
</style>

运行效果

在名字和姓氏输入框中输入内容,控制台会打印出完整名字的变化。

示例 3:深度监听对象

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 深度监听对象</h1>
    <div>
      <label>用户名: </label>
      <input v-model="user.name" placeholder="Name" />
    </div>
    <div>
      <label>城市: </label>
      <input v-model="user.address.city" placeholder="City" />
    </div>
    <p>用户信息:</p>
    <pre>{{ user }}</pre>
  </div>
</template>

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

const user = reactive({
  name: 'John',
  address: {
    city: 'New York',
  },
});

// 深度监听user对象的变化
watch(
  () => user,
  (newUser, oldUser) => {
    console.log('用户信息发生变化:', newUser, oldUser);
  },
  { deep: true }
);
</script>

<style scoped>
h1 {
  color: #42b983;
}
div {
  margin-bottom: 10px;
}
label {
  margin-right: 10px;
}
input {
  padding: 5px;
  font-size: 14px;
}
pre {
  background-color: #f0f0f0;
  padding: 10px;
}
</style>

运行效果

修改用户名或城市,控制台会打印出用户对象的新旧值。

示例 4:立即执行回调函数

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 立即执行回调函数</h1>
    <div>
      <label>输入框: </label>
      <input v-model="inputValue" placeholder="输入内容" />
    </div>
    <p>输入内容: {{ inputValue }}</p>
  </div>
</template>

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

const inputValue = ref('初始值');

// 监听inputValue,并立即执行回调
watch(
  inputValue,
  (newVal, oldVal) => {
    console.log(`inputValue从${oldVal}变为${newVal}`);
  },
  { immediate: true }
);
</script>

<style scoped>
h1 {
  color: #42b983;
}
div {
  margin-bottom: 10px;
}
label {
  margin-right: 10px;
}
input {
  padding: 5px;
  font-size: 14px;
}
</style>

运行效果

页面加载时,watch 的回调会立即执行,打印出初始值的信息。之后,修改输入框内容时,回调也会随之执行。

示例 5:清理副作用

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 清理副作用</h1>
    <button @click="start">开始计时</button>
    <button @click="stop">停止计时</button>
    <p>计时器状态: {{ isRunning ? '运行中' : '已停止' }}</p>
  </div>
</template>

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

const isRunning = ref(false);
let timer = null;

// 监听isRunning的变化
watch(
  isRunning,
  (newVal, oldVal, onCleanup) => {
    if (newVal) {
      // 开始计时
      timer = setInterval(() => {
        console.log('计时中...');
      }, 1000);

      // 清理函数,停止计时
      onCleanup(() => {
        clearInterval(timer);
        console.log('计时已停止。');
      });
    }
  }
);

// 开始计时
const start = () => {
  isRunning.value = true;
};

// 停止计时
const stop = () => {
  isRunning.value = false;
};
</script>

<style scoped>
h1 {
  color: #42b983;
}
button {
  padding: 10px 20px;
  margin-right: 10px;
  font-size: 16px;
}
p {
  margin-top: 20px;
  font-size: 18px;
}
</style>

运行效果

点击“开始计时”按钮,控制台每秒打印一次“计时中...”。点击“停止计时”按钮,计时停止,并在控制台打印“计时已停止。”。

示例 6:结合 watchEffect 使用

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 结合 watchEffect 使用</h1>
    <div>
      <label>输入A: </label>
      <input v-model="a" placeholder="输入A" />
    </div>
    <div>
      <label>输入B: </label>
      <input v-model="b" placeholder="输入B" />
    </div>
    <p>计算结果: {{ a + b }}</p>
  </div>
</template>

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

const a = ref(0);
const b = ref(0);

// 使用 watch 监听a的变化
watch(a, (newVal, oldVal) => {
  console.log(`a从${oldVal}变为${newVal}`);
});

// 使用 watchEffect 自动收集依赖
watchEffect(() => {
  console.log(`watchEffect: a + b = ${a.value + b.value}`);
});
</script>

<style scoped>
h1 {
  color: #42b983;
}
div {
  margin-bottom: 10px;
}
label {
  margin-right: 10px;
}
input {
  padding: 5px;
  font-size: 14px;
  width: 200px;
}
p {
  font-size: 18px;
}
</style>

运行效果

输入框A和B的内容变化时,控制台会分别打印watchwatchEffect的相关信息。

示例 7:自定义 watch 实现原理简述

为了更深入地理解 watch 的实现原理,可以通过一个简化版的自定义 watch 函数来模拟其工作机制。

App.vue

<!-- watch-demo/App.vue -->
<template>
  <div>
    <h1>Vue 3 Watch 示例 - 自定义 watch 实现原理</h1>
    <p>计数: {{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>

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

// 简化版的 Dep 类,用于依赖收集和触发
class Dep {
  constructor() {
    this.subscribers = new Set();
  }

  depend() {
    if (Dep.target) {
      this.subscribers.add(Dep.target);
    }
  }

  notify() {
    this.subscribers.forEach(sub => sub());
  }
}

// 全局 target,用于保存当前正在收集依赖的函数
Dep.target = null;

// 创建一个响应式对象
function reactiveObject(obj) {
  const depsMap = new Map();

  return new Proxy(obj, {
    get(target, key) {
      if (!depsMap.has(key)) {
        depsMap.set(key, new Dep());
      }
      const dep = depsMap.get(key);
      dep.depend();
      return target[key];
    },
    set(target, key, value) {
      target[key] = value;
      if (depsMap.has(key)) {
        const dep = depsMap.get(key);
        dep.notify();
      }
      return true;
    },
  });
}

// 简化版的 watch 实现
function customWatch(source, cb) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else {
    getter = () => source.value;
  }

  let oldValue;

  const update = () => {
    const newValue = getter();
    cb(newValue, oldValue);
    oldValue = newValue;
  };

  Dep.target = update;
  oldValue = getter();
  Dep.target = null;
}

// 使用自定义 watch
const state = reactiveObject({
  count: 0,
});

customWatch(
  () => state.count,
  (newVal, oldVal) => {
    console.log(`customWatch: count从${oldVal}变为${newVal}`);
  }
);

// 增加计数的方法
const increment = () => {
  state.count++;
};
</script>

<style scoped>
h1 {
  color: #42b983;
}
button {
  padding: 10px 20px;
  font-size: 16px;
}
p {
  font-size: 18px;
}
</style>

运行效果

点击“增加计数”按钮,控制台会打印出自定义 watch 函数的变化信息,模拟了 Vue 3 watch 的基本工作流程。

说明

  1. Dep 类:用于管理依赖。每个响应式属性对应一个 Dep 实例,存储订阅者(响应式函数)。
  2. reactiveObject 函数:创建一个简化版的响应式对象,使用 Proxy 进行拦截,并在 getset 操作时进行依赖收集和触发。
  3. customWatch 函数:模拟 watch 的基本功能,接受一个数据源和回调函数。当数据源变化时,执行回调。
  4. 使用示例:创建一个响应式状态对象 state,通过 customWatch 监听 state.count 的变化,并在变化时打印信息。

此示例展示了 watch 的基本原理,包括依赖收集和变化触发,但省略了许多 Vue 3 中的复杂优化和功能。

总结

watch 是 Vue 3 中用于监听数据变化并执行副作用的强大工具。它在 Composition API 中得到了广泛应用,提供了灵活的配置选项和良好的类型支持。通过理解 watch 的内部实现原理和实际应用场景,开发者可以更高效地管理组件中的副作用,提高代码的可维护性和扩展性。

关键点总结:

  • 响应式系统watch 基于 Vue 3 的响应式系统,通过依赖收集和触发机制实现数据监听。
  • 使用方法:支持监听单个或多个数据源,提供深度监听和立即执行等选项。
  • 优缺点watch 提供了灵活的副作用管理,但需要注意配置选项和潜在的性能开销。
  • 实战应用:通过多个详细的代码示例,展示了 watch 在不同场景下的使用方法和效果。

掌握 watch 的使用,能够帮助开发者更好地处理复杂的数据依赖和副作用需求,提升 Vue 应用的整体性能和用户体验。

参考资料

  1. Vue 3 官方文档 - Watchers
  2. Vue 3 源码解析 - Watchers
  3. 《深入浅出Vue.js》
  4. Vue Mastery - Watch Guide
  5. Vue 3 Composition API: using watch
  6. Understanding Vue.js Reactivity
  7. MDN Web 文档 - Proxy
  8. JavaScript 异步编程指南
  9. TypeScript 与 Vue 3 集成
  10. Vue.js 源码学习