Vue3 引入了组合式 API,其中 watchEffectwatch 是两个非常重要的响应式 API,用于在响应式数据发生变化时执行特定的副作用。本文将详细探讨 watchEffectwatch 的实现原理、在项目中的应用、实际使用技巧以及如何手写类似功能的代码。

目录

  1. watchEffect 与 watch
  2. 官方文档解读
  3. watchEffect 与 watch
  4. 项目中的实际应用
  • 数据同步
  • 复杂逻辑处理
  • 性能优化
  • 调试和日志
  • 异步操作
  1. 手写 watchEffect 与 watch
  • 手写 watchEffect
  • 手写 watch
  1. 实际项目中的使用技巧
  • 避免无限循环
  • 合理设置选项
  • 结合其他 API 使用
  • 处理复杂数据结构
  1. 更多实际应用案例
  • 表单验证
  • 数据持久化
  • 组件间通信
  • 动画效果
  1. 总结

watchEffectwatch 简介

watchEffect

watchEffect 是一个立即执行的副作用函数,当其依赖的响应式数据变化时,副作用函数会重新运行。它类似于 Vue2 中的 computedwatch 的结合,但使用更简单。

watch

watch 则更为灵活,它允许显式地指定依赖的响应式数据,并提供了更多的选项来控制副作用的执行时机和方式。

官方文档解读

为了更好地理解这两个 API,我们首先来看看官方文档的描述:

官方文档详细介绍了这两个 API 的用法和参数配置。watchEffect 更适合简单的副作用逻辑,而 watch 则适用于需要精细控制依赖变化和执行逻辑的场景。

watchEffectwatch 的实现原理

watchEffect 的实现原理

watchEffect 的核心在于其依赖自动收集机制。当副作用函数执行时,Vue 会自动追踪其中访问的响应式数据,并在这些数据变化时重新执行副作用函数。

实现步骤:

  1. 依赖收集:当副作用函数访问响应式数据时,将其注册到依赖集合中。
  2. 触发更新:当响应式数据变化时,依赖集合中的副作用函数会被重新执行。

代码示例:

function watchEffect(effect) {
  // 用于存储依赖的副作用函数
  const deps = new Set();

  // 包装 effect 函数以便重新运行
  const runner = () => {
    // 清空之前的依赖
    deps.clear();
    // 运行副作用函数,并记录新依赖
    effect();
  };

  // 模拟响应式依赖追踪
  // 每次获取响应式数据时,注册依赖
  const reactiveHandler = {
    get(target, key, receiver) {
      deps.add(runner);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 触发依赖的副作用函数重新执行
      deps.forEach(dep => dep());
      return result;
    }
  };

  // 创建响应式对象的代理
  const reactive = (obj) => new Proxy(obj, reactiveHandler);

  // 初始化时运行副作用函数
  runner();

  return reactive;
}

watch 的实现原理

watch 的实现更加复杂,允许对依赖数据的变化进行细粒度控制。它通过比较新旧值来决定是否触发回调函数。

实现步骤:

  1. 依赖收集:同样通过依赖追踪机制来收集响应式数据。
  2. 新旧值比较:在数据变化时,通过比较新旧值来决定是否执行回调。
  3. 回调执行:根据配置选项执行回调函数。

代码示例:

function watch(source, callback, options = {}) {
  let oldValue, newValue;
  // 存储依赖的回调函数
  const deps = new Set();

  // 包装回调函数以便执行和记录新旧值
  const runner = () => {
    newValue = source();
    if (newValue !== oldValue || options.deep) {
      callback(newValue, oldValue);
      oldValue = newValue;
    }
  };

  // 模拟响应式依赖追踪
  // 每次获取响应式数据时,注册依赖
  const reactiveHandler = {
    get(target, key, receiver) {
      deps.add(runner);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 触发依赖的回调函数重新执行
      deps.forEach(dep => dep());
      return result;
    }
  };

  // 创建响应式对象的代理
  const reactive = (obj) => new Proxy(obj, reactiveHandler);

  // 根据选项决定是否立即执行回调函数
  if (options.immediate) {
    runner();
  } else {
    oldValue = source();
  }

  return reactive;
}

项目中的实际应用

数据同步

在表单输入与后台数据同步时,watchEffect 可以简化实现:

import { ref, watchEffect } from 'vue';

const data = ref('');
watchEffect(() => {
  console.log(`数据变化:${data.value}`);
});

复杂逻辑处理

在需要复杂逻辑处理时,使用 watch 更加合适:

import { ref, watch } from 'vue';

const count = ref(0);
watch(count, (newValue, oldValue) => {
  console.log(`count 变化:从 ${oldValue} 到 ${newValue}`);
}, { immediate: true });

性能优化

在大型应用中,合理使用 watchwatchEffect 可以提升性能。例如,通过控制依赖收集的粒度,减少不必要的计算和 DOM 更新。

调试和日志

watchwatchEffect 也可以用于调试和日志记录。在开发阶段,通过打印响应式数据的变化,可以更直观地了解数据流动和应用状态。

异步操作

在处理异步操作时,watch 提供了更多的灵活性。例如,可以在回调函数中处理 API 请求,并根据响应数据更新视图。

import { ref, watch } from 'vue';

const userId = ref(1);
const userData = ref(null);

watch(userId, async (newId) => {
  userData.value = await fetchUserData(newId);
}, { immediate: true });

async function fetchUserData(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

手写 watchEffectwatch 的实现

手写 watchEffect

下面是一个更详细的 watchEffect 实现,加入了更多细节来展示其核心原理。

function watchEffect(effect) {
  // 用于存储依赖的副作用函数
  const deps = new Set();

  // 包装 effect 函数以便重新运行
  const runner = () => {
    // 清空之前的依赖
    deps.clear();
    // 运行副作用函数,并记录新依赖
    effect();
  };

  // 模拟响应式依赖追踪
  // 每次获取响应式数据时,注册依赖
  const reactiveHandler = {
    get(target, key, receiver) {
      deps.add(runner);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 触发依赖的副作用函数重新执行
      deps.forEach(dep => dep());
      return result;
    }
  };

  // 创建响应式对象的

代理
  const reactive = (obj) => new Proxy(obj, reactiveHandler);

  // 初始化时运行副作用函数
  runner();

  return reactive;
}

// 使用示例
const state = watchEffect(() => {
  console.log(state.value);
});

state.value = 1; // 触发副作用函数
state.value = 2; // 再次触发副作用函数

手写 watch

下面是一个更详细的 watch 实现,包含更多的实现细节和选项处理。

function watch(source, callback, options = {}) {
  let oldValue, newValue;
  // 存储依赖的回调函数
  const deps = new Set();

  // 包装回调函数以便执行和记录新旧值
  const runner = () => {
    newValue = source();
    if (newValue !== oldValue || options.deep) {
      callback(newValue, oldValue);
      oldValue = newValue;
    }
  };

  // 模拟响应式依赖追踪
  // 每次获取响应式数据时,注册依赖
  const reactiveHandler = {
    get(target, key, receiver) {
      deps.add(runner);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 触发依赖的回调函数重新执行
      deps.forEach(dep => dep());
      return result;
    }
  };

  // 创建响应式对象的代理
  const reactive = (obj) => new Proxy(obj, reactiveHandler);

  // 根据选项决定是否立即执行回调函数
  if (options.immediate) {
    runner();
  } else {
    oldValue = source();
  }

  return reactive;
}

// 使用示例
const count = watch(
  () => count.value,
  (newValue, oldValue) => {
    console.log(`Count changed from ${oldValue} to ${newValue}`);
  },
  { immediate: true }
);

count.value = 1; // 触发回调
count.value = 2; // 再次触发回调

实际项目中的使用技巧

避免无限循环

在使用 watchEffectwatch 时,需要特别注意避免无限循环。例如,当副作用函数中直接修改响应式数据时,可能会导致无限循环。为了避免这种情况,可以在副作用中使用条件判断来控制数据更新。

import { ref, watchEffect } from 'vue';

const count = ref(0);
watchEffect(() => {
  if (count.value < 10) {
    count.value++;
  }
});

合理设置选项

watch 提供了多种选项来控制回调的执行时机和方式。例如,可以通过设置 immediate 选项让回调在初始化时立即执行,通过 deep 选项实现对嵌套对象的深度监听。

import { ref, watch } from 'vue';

const user = ref({ name: 'John', age: 30 });

watch(user, (newValue, oldValue) => {
  console.log('User data changed:', newValue);
}, { deep: true, immediate: true });

结合其他 API 使用

在实际项目中,watchEffectwatch 常常需要与其他 Vue API 结合使用。例如,可以与 computed 结合,实现复杂的计算逻辑和副作用处理。

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

const num1 = ref(1);
const num2 = ref(2);
const sum = computed(() => num1.value + num2.value);

watchEffect(() => {
  console.log(`Sum: ${sum.value}`);
});

处理复杂数据结构

当处理复杂的数据结构时,可以使用 watchdeep 选项实现深度监听。此外,还可以使用自定义的比较函数来优化性能。

import { ref, watch } from 'vue';

const data = ref({
  user: {
    name: 'John',
    address: {
      city: 'New York',
      zip: '10001'
    }
  }
});

watch(() => data.value.user, (newValue, oldValue) => {
  console.log('User data changed:', newValue);
}, { deep: true });

更多实际应用案例

表单验证

在表单验证中,watchEffectwatch 可以用于实时验证用户输入。例如:

import { ref, watch } from 'vue';

const username = ref('');
const usernameError = ref('');

watch(username, (newValue) => {
  if (newValue.length < 3) {
    usernameError.value = '用户名长度必须大于 3';
  } else {
    usernameError.value = '';
  }
});

数据持久化

在应用中,需要将数据持久化到本地存储时,可以使用 watch 监控数据变化并进行保存:

import { ref, watch } from 'vue';

const settings = ref({
  theme: 'dark',
  notifications: true
});

watch(settings, (newValue) => {
  localStorage.setItem('settings', JSON.stringify(newValue));
}, { deep: true });

// 初始化时从本地存储读取
const storedSettings = JSON.parse(localStorage.getItem('settings'));
if (storedSettings) {
  settings.value = storedSettings;
}

组件间通信

在父子组件通信时,可以使用 watch 监听父组件传递的 props 并进行处理:

父组件:

<template>
  <ChildComponent :data="parentData" />
</template>

<script>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';

export default {
  components: { ChildComponent },
  setup() {
    const parentData = ref('父组件数据');
    return { parentData };
  }
};
</script>

子组件:

<template>
  <div>{{ processedData }}</div>
</template>

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

export default {
  props: ['data'],
  setup(props) {
    const processedData = ref('');
    
    watch(() => props.data, (newData) => {
      processedData.value = newData + ' - 已处理';
    });

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

动画效果

在处理动画效果时,可以使用 watchEffect 动态监听数据变化并触发动画:

import { ref, watchEffect } from 'vue';

const isVisible = ref(false);

watchEffect(() => {
  if (isVisible.value) {
    document.getElementById('animatedElement').classList.add('fade-in');
  } else {
    document.getElementById('animatedElement').classList.remove('fade-in');
  }
});

// 在模板中绑定 isVisible 来控制动画效果
<template>
  <div id="animatedElement" :class="{ 'fade-in': isVisible }">动画元素</div>
  <button @click="isVisible = !isVisible">切换动画</button>
</template>

总结

watchEffectwatch 是 Vue3 中两个非常强大的响应式 API,它们分别适用于简单和复杂的副作用处理。在项目中合理使用这两个 API,可以极大地提升代码的可维护性和性能。通过手写简化版的 watchEffectwatch 实现,我们更加深入理解了其背后的实现原理。本文还介绍了在实际项目中使用这两个 API 的技巧和注意事项,帮助开发者更好地掌握和应用它们。

官方文档链接:watchEffect, watch

希望这篇博客能够帮助你深入了解 watchEffectwatch 的实现原理和应用场景。如果有任何疑问或需要进一步的探讨,欢迎留言交流。