初探 Vue 3响应式源码(四):Watch

300 阅读5分钟

无论Vue2还是 Vue3 ,watch 无论在使用还是面试中都是当之无愧的高频,它让我们能够监听响应式数据的变化,并在变化时执行相应的回调函数,今天和大家一起学习下watch的内部实现。

1. watch 的基本用法

在开始解析源码之前,我们先回顾一下 watch 的基本用法。

watch 可以监听一个或多个响应式数据,并在它们发生变化时执行回调函数。

import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

count.value++; // 输出: 从 0 到 1

2. watch 的核心实现

2.1 watch 函数的定义

watch 函数的定义如下:

function watch(source, cb, options) {
  // 处理 source 和 cb
  // 创建 effect
  // 返回停止监听的函数
}

watch 函数接收三个参数:

  • source:要监听的数据源,可以是一个响应式对象、ref、或者一个 getter 函数。
  • cb:回调函数,当 source 发生变化时执行。
  • options:可选的配置项,比如 immediatedeep 等。

2.2 处理 source


watch 函数首先需要处理 source,将其转换为一个 getter 函数。这是因为 Vue3 的响应式系统是基于 effect 的,而 effect 需要一个 getter 函数来追踪依赖。

function watch(source, cb, options) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (isReactive(source)) {
    getter = () => source;
    options.deep = true; // 默认深度监听
  } else {
    getter = () => {};
  }

  // 其他逻辑
}

在这个代码片段中,我们根据 source 的类型来决定如何生成 getter 函数:

  • 如果 source 是一个函数,直接将其作为 getter
  • 如果 source 是一个 ref,则 getter 返回 ref.value
  • 如果 source 是一个 reactive 对象,则 getter 返回该对象,并默认启用深度监听。

2.3 初始化环境

首先,watch需要为后续的操作做好准备。这包括定义一个用于存储清理函数的变量和创建一个job函数,这个函数将在数据变化时被调用。

接下来,watch会创建一个ReactiveEffect实例。这个实例是Vue3响应式系统的核心,它负责追踪source的依赖,并在依赖变化时执行job函数。

function watch(source, cb, options) {
    // ...上面的getter逻辑

    let cleanup; // 用于存储清理函数

    // 定义一个函数,用于注册清理函数
    const onInvalidate = (fn) => {
      cleanup = fn;
    };

    // 定义 job 函数,它会在依赖变化时执行
    const job = () => {
      if (cleanup) {
        cleanup(); // 如果存在清理函数,则先执行清理函数
      }
      const newValue = effect.run(); // 执行 effect 来获取新的值
      cb(newValue, oldValue, onInvalidate); // 调用回调函数
      oldValue = newValue; // 更新旧值
    };
    // 创建一个 ReactiveEffect 实例,当getter中依赖的数据变化,就会执行job ⭐️
    const effect = new ReactiveEffect(getter, job);

    // 首次执行 effect 来获取初始值 
    let oldValue = effect.run();
}

2.4 处理 immediate 选项

watch 还支持 immediate 选项,当 immediatetrue 时,回调函数会在 watch 创建时立即执行一次。

最后,watch会根据immediate选项决定是否立即执行job函数。同时,它返回一个函数,允许我们手动停止对source的监听。

if (options.immediate) {
  job();
} else {
  oldValue = effect.run();
}

在这个代码片段中,我们根据 immediate 选项来决定是否立即执行 job 函数。

2.5 返回停止监听的函数

最后,watch 函数返回一个停止监听的函数,调用这个函数可以停止对 source 的监听。

return () => {
  effect.stop();
};

这个函数通过调用 effect.stop() 来停止 effect 的运行,从而停止对 source 的监听。

2.6 完整代码

function watch(source, cb, options) {
    let getter;
    if (typeof source === 'function') {
      getter = source;
    } else if (isRef(source)) {
      getter = () => source.value;
    } else if (isReactive(source)) {
      getter = () => source;
      options.deep = true; // 默认深度监听
    } else {
      getter = () => {};
    }
  
    let cleanup; // 用于存储清理函数

    // 定义一个函数,用于注册清理函数
    const onInvalidate = (fn) => {
      cleanup = fn;
    };

    // 定义 job 函数,它会在依赖变化时执行
    const job = () => {
      if (cleanup) {
        cleanup(); // 如果存在清理函数,则先执行清理函数
      }
      const newValue = effect.run(); // 执行 effect 来获取新的值
      cb(newValue, oldValue, onInvalidate); // 调用回调函数
      oldValue = newValue; // 更新旧值
    };
    // 创建一个 ReactiveEffect 实例 
    const effect = new ReactiveEffect(getter, job);

    // 首次执行 effect 来获取初始值 
    let oldValue = effect.run();
    
    // 如果选项中设置了 immediate 为 true,则立即执行 job ⭐️新增
    if (options.immediate) { 
      job();
    } else {
      // 否则,再次执行 effect 来确保 oldValue 是响应式数据的当前值
      oldValue = effect.run();
    }

    // 返回一个函数,调用它可以停止 effect,从而停止对 source 的监听 ⭐️新增
    return () => {
      effect.stop();
    };
}

3. 代码测试

watch部分就自动中就用我们文章最下方的附录代码

3.1 测试普通 ref

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}
const count = ref(0);


watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

count.value++; // 输出: 从 0 到 1

在这个例子中,watch 监听了 count 的变化,并在 count 的值发生变化时打印出新旧值。

3.2 测试 reactive 对象

import { reactive, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const state = reactive({ count: 0 });

watch(() => state.count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

state.count++; // 输出: 从 0 到 1

在这个例子中,watch 监听了 state.count 的变化,并在 state.count 的值发生变化时打印出新旧值。

3.3 使用 immediate 选项

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
}, { immediate: true });

// 输出: 从 undefined 到 0
count.value++; // 输出: 从 0 到 1

在这个例子中,watch 在创建时立即执行了一次回调函数,输出了 count 的初始值。

3.4 使用 onInvalidate 清理副作用

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const count = ref(0);

watch(count, (newValue, oldValue, onInvalidate) => {
  let expired = false;
  onInvalidate(() => {
    expired = true;
  });

  setTimeout(() => {
    if (!expired) {
      console.log(`从 ${oldValue}${newValue}`);
    }
  }, 1000);
});

count.value++; // 1秒后输出: 从 0 到 1
count.value++; // 不会输出,因为上一次的副作用被清理了

在这个例子中,我们使用 onInvalidate 来清理上一次的副作用,确保只有最新的回调函数会执行。

3. 总结

对于watch的解析就到这里咯,有什么问题的话,感谢指正!