vue3源码阅读与实现: 响应式系统-watch模块

71 阅读8分钟

本文是专栏的第五篇,上一篇地址为# vue3源码阅读与实现: 响应式系统-computed模块

watch模块

模块概览

image-20240808104325749.png

侦听器watch可以在响应式状态每次发生变化的时候触发回调函数.和计算属性思想类似:在状态发生变化的时候,自动做一些其他事情.

他有以下特性:

  1. 状态变化,自动执行回调
  2. 回调的执行时机: 默认在父组件更新之后,所属组件的DOM更新之前被调用
  3. 数据更新时,同一个侦听器回调函数会被批量处理,也就是说,同步修改数据1000次,侦听器的回调只会触发一次,且数据以最后一次为准

让我们从源码中,理解这些特性的实现原理吧

debugger

使用如下的测试用例:

...
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect, watch, ref } = Myvue;
      const obj = reactive({ name: "响应式" });
        
      debugger
      watch( // 关注点1: 查看watch函数执行时,做了哪些事情
        obj,
        (oldValue, newValue) => {
          console.log("检测到值变化了,oldValue,newValue:", oldValue, newValue);
        },
      );
​
      setTimeout(() => {
        debugger
        obj.name = "第一次修改"; // 关注点2: 数据变化时,如何触发回调执行
      }, 2000);
    </script>
  </body>
</html>

同样的,有以下关注点:

  1. 关注点1: 查看watch函数执行时,做了哪些事情,如何触发依赖收集
  2. 关注点2: 数据变化时,如何触发依赖将回调函数执行

关注点1:watch的创建和触发依赖收集

进入第一个debugger;

  1. 进入watch进行了简单判断后进入doWatch函数,其接收三个参数: doWatch(source:侦听的数据源,cb: 回调函数,options:配置)

  2. doWatch中首先根据数据源类型,初始化getter函数

image-20240717105546032.png

  1. 接着通过traverse函数处理了一下getter函数,这个traverse函数这里先提一下,它的作用就是触发响应式数据的依赖收集

image-20240717105705174.png

  1. 然后定义了oldValue变量,回调函数中的第一个参数就是它,之后定义了一个函数job,在job中执行了effect.run(),此时真正的effect还没有创建出来,但可以大胆猜测执行这个job就会执行watch的回调函数

image-20240717110531543.png

  1. 接着定义了调度器scheduler并赋值,调度器的内容暂时看作执行job即可.这里的调度器和computed中异曲同工,都是希望数据变化时,执行指定的scheduler,而不是run函数.这里的queuePreFlush是进行异步调度任务的函数,watch回调函数的执行时机就是由它来控制.这里不研究具体实现,知道有这么个东西就行了,在关注点2执行该函数的时候会仔细查看

image-20240717110657377.png

  1. 紧接着创建了ReactiveEffect实例,将上面处理好的getterscheduler传递给构造函数,读过上一篇文章computed的同学看到这个代码应该能想到一件事情: effect调用run函数实际就是触发了getter会触发依赖收集,某个响应式数据会将effect收集起来,依赖触发时由于传递了scheduler,就优先执行scheduler,

image-20240717111155276.png

  1. 之后就是处理配置项的逻辑,配置项处理后执行了effect.run,其实就是执行了getter函数触发响应式数据的依赖收集

image-20240717112857362.png

  1. 触发了run函数,相等于执行了上述初始化的getter(),getter()通过traverse函数包装,进入该函数,可以看到,这里根据数据源的类型,递归访问了所有数据,从而触发响应式数据的依赖收集,这样一来,在第6步创建的effect就会被收集.到此完成了依赖收集的过程,侦听器和数据源成功建立了联系

image-20240808093902913.png

  1. 最后返回一个函数,总内容可以看出来是停止数据监听相关的逻辑

image-20240717112942134.png

总结

watch函数中,主要做了四件事:

  1. 根据数据源类型,初始化getter函数,getter函数的执行可以触发响应式数据的依赖收集,同时获取到最新的响应式数据
  2. 初始化job函数
  3. 初始化scheduler,将来依赖触发的时候,执行scheduler中的逻辑
  4. 创建ReactiveEffect实例,这是实现响应式的关键实例,依赖收集就会把这个带有scheduler的effect实例收集起来

关注点2:watch的依赖触发

进入下一个debugger:

  1. 设置响应式数据,触发setter,开始进行依赖触发,这里的逻辑已经在reactive,ref中提到很多次了,我们直接来到triggerEffects函数,查看当前触发的依赖是什么:

image-20240717114710091.png

  1. 可以看到,这个effets中存放的就是在watch函数执行时创建的ReactivcEffect实例

  2. 继续执行,由于scheduler优先级高,所以会触发scheduler函数,

image-20240717114857044.png

  1. 进入scheduler,就来到在关注点1时,创建的scheduler:

image-20240717115002919.png

  1. 进入queuePreFlushCb,之后会进入queueCb,在这里没有立即执行任务,而是把任务放在了一个队列中pendingQueue,然后触发queueFlush方法

image-20240717115058034.png

  1. 进入queueFlush,这里有一个判断!isFlushing && !isFlushPending意思可以理解为是否正在执行任务队列,或者等待执行,如果没有,则使用Promise.resolveflushJobs包装成一个微任务放进js的异步任务队列中,等待同步代码执行完毕,在执行这个任务

image-20240717115415957.png

  1. 先来看看这个flushJobs做了什么事情:设置状态,然后进入flushPreFlushCbs,

image-20240717115907065.png

  1. flushPreFlushCbs,先对任务队列去重,然后遍历执行了任务队列中的所有任务,就是之前保存的job函数:

image-20240717120102540.png

  1. job函数中,首先重新获取响应式数据,并比较响应式数据是否变化,如果发生变化,在callWithAsyncErrorHandling中重新执行watch的回调函数,完成依赖触发

image-20240808101805440.png

总结

在这个过程中watch做了两件事

  1. 通过调度器,执行回调
  2. 通过异步队列,控制回调函数的执行.把所有触发的回调函数都在放在队列中,然后异步执行的原因: vue不希望同步将一千个项目推入被侦听的数组中,侦听器被同步触发一千次,因此,在异步中处理所有的job,就能保证所有同步代码触发watch的代码全部执行完毕,然后对任务队列中进行去重处理,保证多次修改数据,侦听器的回调只被触发一次

总结

整个源码读下来,可以对比构建reactive模块时effect函数来更容易理解:

effect(fn)

  • 执行fn进行触发依赖收集,依赖触发重新执行fn

watch(getter,fn)

  • 执行getter进行依赖收集,依赖触发执行fn

源码整体分为四块:

  1. watch模块
  2. getter: 执行这个函数,可以触发依赖收集
  3. flushJobs: 执行异步任务队列
  4. job: 在这里记录oldValue,newValue,并执行回调

实现watch模块

watch函数

侦听器的主函数,这个函数中主要进行:

  1. 根据数据源创建getter函数
  2. 创建调度器
  3. 生成ReactiveEffect实例
  4. 处理配置项相关的逻辑

packages/runtime-core/src/eapiWatch.ts中:

import { EMPTY_OBJ, hasChange, isObject, isReactive, isRef } from "@vue/shared";
import {
  EffectScheduler,
  ReactiveEffect,
} from "packages/reactivity/src/effect";
import { queuePreFlushCb } from "./scheduler";
​
// watch配置项类型
export interface WatchOptions<Immediate = boolean> {
  immediate?: Immediate;
  deep?: boolean;
}
​
export function watch(source, cb: Function, options?: WatchOptions) {
  return toWatch(source, cb, options);
}
​
/**
 * @message: 主函数
 */
function toWatch(
  source,
  cb: Function,
  { immediate, deep }: WatchOptions = EMPTY_OBJ
) {
  // 处理getter,最终把getter包装成可以触发依赖收集的函数
  let getter;
  if (isReactive(source)) {
    getter = () => source;
    deep = true;
  } else if (isRef(source)) {
    debugger;
    getter = () => source.value;
  } else {
    getter = () => ({});
  }
  if (cb && deep) {
    const baseGetter = getter;
    getter = () => traverse(baseGetter());
  }
​
  // 定义job
  const job = () => {
    if (cb) {
      const newValue = effect.run(); // 当数据变化时获取最新的值
      if (deep || hasChange(oldValue, newValue)) {
        cb(oldValue, newValue);
        oldValue = newValue;
      }
    }
  };
  // 定义调度器
  const scheduler: EffectScheduler = () => queuePreFlushCb(job);
​
  let oldValue = {}; // 赋值为一个空对象,便于: Immediate为true时,hasChange为true,保证顺利执行cb
  // 创建effect
  let effect = new ReactiveEffect(getter, scheduler);
​
  if (cb) {
    if (immediate) {
      job();
    } else {
      oldValue = effect.run();
    }
  }
​
  return () => {
    effect.stop();
  };
}
​
/**
 * @message: 递归访问数据的所有属性,从而触发依赖收集
 */
function traverse(value: any) {
  if (isObject(value)) {
    for (let key in value) {
      traverse(value[key]);
    }
  }
  return value;
}

调度器逻辑

调度器主要的作用是控制代码的执行顺序:

  1. 通过异步来实现任务的延迟执行
  2. 通过去重操作,防止任务的重复执行

packages/runtime-core/src/scheduler.ts

let isFlushPending = false;
// 等待执行jobs
const pendingPreFlushCbs: Function[] = [];
// 当前正在执行的promise
let currentFlushPromise: Promise<void> | null = null;
// 创建一个成功状态的promise
const resolvedPromise = Promise.resolve() as Promise<any>;
​
export function queuePreFlushCb(cb: Function) {
  queueCb(cb, pendingPreFlushCbs);
}
​
function queueCb(cb: Function, pendingQueue: Function[]) {
  pendingQueue.push(cb);
  queueFlush();
}
​
function queueFlush() {
  if (!isFlushPending) {
    isFlushPending = true;
    currentFlushPromise = resolvedPromise.then(flushJobs); // 相当于向微任务队列放了一个任务,同步代码执行完就会执行该任务
  }
}
​
function flushJobs() {
  isFlushPending = false;
  flushPreFlushCbs();
}
​
/**
 * @message: 真正开始执行job
 */
function flushPreFlushCbs() {
  if (pendingPreFlushCbs.length) {
    const activePreFlushCbs = [...new Set(pendingPreFlushCbs)]; // 去重以保证任务不重复执行
    pendingPreFlushCbs.length = 0;
    activePreFlushCbs.forEach((item) => item());
  }
}

总结

watch的核心思路就是这个样子,本质上还是依赖收集,依赖触发,只不过这里的依赖收集需要主动通过traverse进行触发,依赖触发时还使用调度器来控制依赖执行的顺序.

到此响应式系统涉及的内容基本结束了,总结ref,reactive,computed,watch可以看出,响应式系统实现的合适是ReactiveEffect这个类,所以对响应式系统实现依然比较模糊时,应该重点关注个这个类的实现(在第二篇文章reactive模块中,有讲解).