Vue2 - Watch 侦听器源码理解

Vue 版本 :以 vue@2.7.16 代码为参考

概念理解

Watch 侦听器

Vue 提供的响应式侦听器,用于监听数据的变化,并在变化时执行相应的回调函数。

watch 侦听器适用于需要在数据变化时执行异步操作或复杂的回调操作流程。

在我的理解里,watch 更适合于数据侦听,但如果事件侦听可行的话,还是依赖事件侦听来完成业务逻辑,避免过多的 watch 导致数据流混乱,特别对于大型项目,会导致排查困难。

当然仅限于 Vue2 的场景,Vue3 基于 hook 的逻辑封装,其实可以很大程度避免这个问题。

核心特性

  • 回调执行:支持函数、字符串、数组、对象等多种定义形式,对数据进行侦听,数据变化时执行用户定义的回调函数。
  • 深度监听 :支持监听对象内部属性的变化(deep: true)。
  • 立即执行 :支持在初始化时立即执行一次回调(immediate: true)。

应用场景

  • 异步请求操作:根据数据变化时执行异步请求、API 调用,常用于表格翻页场景。
  • 副作用处理:数据变化时执行 DOM 操作、事件绑定、定时器等副作用操作。
  • 数据验证:监听表单数据变化,进行实时验证,启用保存功能等。
typescript 复制代码
export default {
  data() {
    return {
      message: "Hello Vue!",
      user: {
        name: "John",
        age: 30,
      },
    };
  },
  watch: {
    // 函数形式
    message(newVal, oldVal) {
      console.log(`message changed from ${oldVal} to ${newVal}`);
    },
    // 对象形式(支持深度监听和立即执行)
    user: {
      handler(newVal, oldVal) {
        console.log("User object changed:", newVal);
      },
      deep: true,
      immediate: true,
    },
    // 字符串形式(监听嵌套属性)
    "user.name"(newVal, oldVal) {
      console.log(`User name changed: ${oldVal} -> ${newVal}`);
    },
    // 数组形式(多个回调)
    message: [
      function handler1(newVal, oldVal) {
        console.log("Handler 1:", newVal);
      },
      function handler2(newVal, oldVal) {
        console.log("Handler 2:", newVal);
      },
    ],
  },
};

整体流程概述

Watch 侦听器的完整生命周期包含三个阶段:

  1. 初始化阶段initWatch() 完成 watch 创建,收集依赖。
  2. 依赖变化阶段 :依赖数据变化时触发 update(),执行回调函数。
  3. 清理阶段:组件销毁时清理 Watcher 实例。

下文中的术语说明

  • UserWatcher :watch 创建的 Watcher 实例对象,负责监听数据变化并执行回调,为何叫 UserWatcher,因为源码里使用 options.user = true 来代表由用户显式创建的 Watcher,通常指代的就是 watch 对象
  • RenderWatcher:视图更新 Watcher

初始化阶段

  1. 遍历组件的 watch 选项。
  2. 为每个 watch 属性创建 UserWatcher。
    • 解析 watch 的定义形式(函数、字符串、数组、对象)。
    • 创建 getter 函数(用于收集依赖)。
    • 创建 Watcher 实例(user: true 标记为用户定义的 watcher)。
  3. 如果设置了 immediate: true,立即执行一次回调。
  4. 如果设置了 deep: true,在初始化 Watcher 时通过 traverse() 完成深度监听。

initWatch 和 createWatcher:对参数规范化处理

在进行正式处理前,对整体的参数进行规范化处理,提取回调函数 handler 和配置项 options

typescript 复制代码
// src/core/instance/state.ts
function initWatch(vm: Component, watch: Object) {
  // 遍历组件的 watch 选项
  for (const key in watch) {
    const handler = watch[key];

    // 支持数组形式:多个回调函数
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

// src/core/instance/state.ts
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  // 如果 handler 是对象形式,提取 handler 和 options
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }

  // 如果 handler 是字符串,则从 vm 实例上获取对应的 method 方法
  if (typeof handler === "string") {
    handler = vm[handler];
  }

  // 调用 $watch 创建 Watcher
  return vm.$watch(expOrFn, handler, options);
}

$watch:watch 核心实现

$watch 是 watch 的核心实现,主要完成:

  • 负责创建 Watcher 实例,在创建时根据 deep 声明,决定是否深层访问属性。
  • 根据 immediate 声明,处理立即执行逻辑。

这里源码声明很巧妙的用了 "touch" 这个词,因为就只是递归访问对象或者数组的内容去触发 getter 实现深层依赖收集,让对应可观测对象的 Dep 收集 UserWatcher。

可观测对象:可以理解成带 .__ob__ 属性的值,即创建 Observer 实例的数据,为了实现深层属性响应,Vue2 会为 data 中的对象属性深层递归,创建 Observer。

typescript 复制代码
// Watcher 中相关源码部分
export default class Watcher implements DepTarget {
  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    if (isFunction(expOrFn)) {
      this.getter = expOrFn;
    } else {
      // watch 的属性声明,run 执行的 getter 是走这部分逻辑,也就是去获取对应的依赖属性值
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
      }
    }
    this.value = this.lazy ? undefined : this.get();
  }
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      // 收集依赖项
      value = this.getter.call(vm, vm);
    } catch (e: any) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        // 在 watch 时指定了 deep:true,递归收集依赖
        traverse(value);
      }
      popTarget();
      // 清除旧 dep 依赖收集
      this.cleanupDeps();
    }
    return value;
  }

  run() {
    // active 判断当前 watcher 是活跃状态,没有 teardown()
    if (this.active) {
      // 重新收集依赖并获取最新值
      const value = this.get();
      if (
        // watch 值不相等 / value 是对象时 / 指定 deep:true 时执行
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        // 更新值
        const oldValue = this.value;
        this.value = value;
        // options.user,即当前 Watcher 属于 UserWatcher 时执行 this.cb()
        // 这里是执行我们在应用层 watch 监听时传入的回调函数
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`;
          invokeWithErrorHandling(
            this.cb,
            this.vm,
            [value, oldValue],
            this.vm,
            info
          );
        } else {
          // 触发回调
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  }

  teardown = function () {
    // 判断销毁
    if (this.vm && !this.vm._isBeingDestroyed) {
      // 通过解除引用来销毁
      remove(this.vm._scope.effects, this);
    }

    // 移除 watcher 对 dep 的订阅和 dep 对 watcher 的依赖收集
    if (this.active) {
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  };
}

// src/core/instance/state.ts
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this;

  // 如果 handler 是对象,提取 handler 和 options
  // 这块是针对 this.$watch() 调用场景进行特殊处理
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options);
  }

  options = options || {};
  options.user = true; // 标记为用户定义的 watcher

  // 创建 Watcher 实例
  const watcher = new Watcher(vm, expOrFn, cb, options);

  // 如果设置了 immediate: true,立即执行一次回调
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value, watcher.value);
    } catch (error) {
      handleError(
        error,
        vm,
        `callback for immediate watcher "${watcher.expression}"`
      );
    }
  }

  // 返回取消监听的函数,setup 和直接调用 $watch 场景下使用
  return function unwatchFn() {
    watcher.teardown();
  };
};

依赖收集阶段

执行流程

  1. $watch 创建 Watcher 实例时,会立即执行 watcher.get() 方法。
  2. get() 方法中,将当前 UserWatcher 推入 targetStack,设置 Dep.target = UserWatcher
  3. 执行 getter 函数,访问被监听的响应式数据。
  4. 响应式数据的 getter 触发,将当前 UserWatcher 添加到依赖列表中。
  5. 如果设置了 deep: true,递归遍历对象的所有属性,建立深度依赖关系。
  6. 将 UserWatcher 从 targetStack 弹出,完成依赖收集。

整体执行流程如下:

复制代码
创建 UserWatcher(new Watcher())
  ↓
执行 watcher.get()
  ↓
pushTarget(UserWatcher)  // targetStack = [UserWatcher]
  ↓
执行 getter 函数,访问被监听的响应式数据
  ↓
响应式数据的 getter 触发,UserWatcher 订阅依赖
  ↓
如果 deep: true,执行 traverse() 递归遍历所有属性
  ↓
popTarget()  // targetStack = []
  ↓
cleanupDeps()  // 清除旧依赖,更新依赖列表
  ↓
返回 value

依赖更新阶段

  1. 被监听的响应式数据发生变化,setter 触发。
  2. setter 调用 dep.notify() 通知所有订阅的 Watcher。
  3. UserWatcher 的 update() 方法被调用。
  4. UserWatcher 会和 RenderWatcher 一样,被推入异步队列中等待执行(Vue3 / Vue2.7 setup 模式下支持了通过设置 flush: 'sync' 同步执行)。
  5. 执行 run() 方法,注意当值不相等、值是对象或指定了 deep: true 时,才会重新获取值并比较新旧值。
  6. 如果值发生变化,执行回调函数。

整体执行流程如下:

复制代码
响应式数据 setter 触发
  ↓
dep.notify()
  ↓
通知 UserWatcher.update()
  ↓
推入异步队列(queueWatcher)
  ↓
nextTick 执行
  ↓
执行 watcher.run()
  ↓
重新获取值(watcher.get())
  ↓
比较新旧值
  ↓
如果值发生变化,执行回调函数
  ↓
cb.call(vm, newValue, oldValue)

思考

1. Watch 和 Computed 的区别是什么?
  • Watch:用于监听数据变化并执行副作用(如 API 调用、DOM 操作),支持异步操作,适合需要在数据变化时执行异步操作、复杂业务逻辑、副作用处理的场景。
  • Computed:用于基于响应式数据计算派生值,具有缓存机制,且为同步计算,适合需要基于响应式数据计算派生值,且需要缓存优化的场景。
2. 为什么深度监听会有性能开销?

深度监听通过 traverse() 递归遍历对象的所有属性,访问每个属性时会触发其 getter,从而建立依赖关系。对于大型对象或深层嵌套结构,这会遍历大量属性,带来性能开销。

同时在后续访问只要触发了依赖的 dep.notify(),对应的 UserWatcher 就会执行回调,不会去细粒度判断值变更。

在实现时尽量:

  • 避免对大型对象进行深度监听。
  • 使用字符串路径监听特定属性(如 "user.name")。
3. Watch 的回调函数中,oldValue 和 newValue 为什么可能是同一个对象引用?

在深度监听对象时,如果对象内部属性发生变化,对象本身的引用并没有改变, oldValuenewValue 会指向同一个对象。

所以在做监听时,如果需要比较新旧内容的差异,尽量具体到对应的属性,而不是去监听某个对象引用,避免出现逻辑异常。

4. 如何取消 Watch 监听?

$watch 方法返回一个取消监听的函数,调用该函数即可取消监听:

typescript 复制代码
export default {
  mounted() {
    // 创建 watch 并保存取消函数
    this.unwatch = this.$watch("message", (newVal, oldVal) => {
      console.log("Message changed:", newVal);
    });
  },
  beforeDestroy() {
    // 取消监听
    this.unwatch();
  },
};

总结

  • 核心机制:通过 Watcher 实现响应式监听,依赖数据变化时执行回调函数。
  • 初始化流程initWatchcreateWatcher$watch → 创建 Watcher → 收集依赖。
  • 更新流程 :依赖变化 → dep.notify()watcher.update()watcher.run() → 执行回调。
  • 关键特性:deep 深度监听、immediate 立刻执行。
  • 注意事项
    • 深度监听会在初期监听和后期更新带来额外性能开销,应谨慎使用。
    • 可以通过 $watch 返回的函数取消监听。

参考内容

相关推荐
你疯了抱抱我2 小时前
【QQ】空间说说批量删除脚本(不用任何额外插件,打开F12控制台即可使用)
开发语言·前端·javascript
进击的野人2 小时前
Vuex 详解:现代 Vue.js 应用的状态管理方案
前端·vue.js·前端框架
未知原色2 小时前
前端工程师转型AI的优势与挑战
前端·人工智能
鹏北海2 小时前
Single-SPA 学习总结
前端·javascript·微服务
想学后端的前端工程师2 小时前
【CSS高级技巧与动画实战指南:打造炫酷的用户体验】
前端·css·ux
Aliex_git2 小时前
HTTP 协议发展整理
笔记·网络协议·http
程芯带你刷C语言简单算法题2 小时前
Day37~求组合数
c语言·开发语言·学习·算法·c
跟着珅聪学java2 小时前
NPM镜像切换教程
vue.js
zhangfeng11332 小时前
大语言模型llm学习路线电子书 PDF、开源项目、数据集、视频课程、面试题、工具镜像汇总成一张「一键下载清单」
学习·语言模型·pdf