Skip to content

Watcher

watch 分类

提示

在 Vue 中, 有三种类型的watcher实例, 分别为:

  • watch watcher: 被监听的属性值变化后,执行对应的回调函数
  • computed watcher: 用于控制计算属性是否需要重新计算
  • render watcher: 用于 vue 组件渲染

Watcher定义在 src/core/instance/observer/watcher.ts

Watcher源码如下:

ts
let uid = 0;

export default class Watcher implements DepTarget {
  vm?: Component | null;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before?: Function;
  onStop?: Function;
  noRecurse?: boolean;
  getter: Function;
  value: any;
  post: boolean;

  // dev only
  onTrack?: ((event: DebuggerEvent) => void) | undefined;
  onTrigger?: ((event: DebuggerEvent) => void) | undefined;

  constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    recordEffectScope(
      this,
      // if the active effect scope is manually created (not a component scope), prioritize it
      activeEffectScope && !activeEffectScope._vm
        ? activeEffectScope
        : vm
        ? vm._scope
        : undefined
    );
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this;
    }
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
      if (__DEV__) {
        this.onTrack = options.onTrack;
        this.onTrigger = options.onTrigger;
      }
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.post = false;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression = __DEV__ ? expOrFn.toString() : "";
    // parse expression for getter
    if (isFunction(expOrFn)) {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        __DEV__ &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    this.value = this.lazy ? undefined : this.get();
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    pushTarget(this); // 给 Dep.target 赋值, 也是将watcher实例入栈
    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) {
        traverse(value); // 递归遍历对象以调用所有转换后的 getter,以便将对象内的每个嵌套属性收集为“深度”依赖项。
      }
      popTarget(); // 收集完依赖以后将watcher出栈
      this.cleanupDeps(); // 清除依赖集合
    }
    return value;
  }

  /**
   * Add a dependency to this directive.
   */
  addDep(dep: Dep) {
    const id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp: any = this.depIds;
    this.depIds = this.newDepIds;
    this.newDepIds = tmp;
    this.newDepIds.clear();
    tmp = this.deps;
    this.deps = this.newDeps;
    this.newDeps = tmp;
    this.newDeps.length = 0;
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    if (this.active) {
      const value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value;
        this.value = value;
        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);
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate() {
    this.value = this.get();
    this.dirty = false;
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown() {
    if (this.vm && !this.vm._isBeingDestroyed) {
      remove(this.vm._scope.effects, this);
    }
    if (this.active) {
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
      if (this.onStop) {
        this.onStop();
      }
    }
  }
}

vue 中三种 Watcher 都是服用发这一个 Watcher 类,可能计算属性或者监听器相对来说更好理解,所以先从这两者来分析 Watcher 类

一. 监听器 watcher

监听器的初始化过程见下一篇《监听器 watch 原理探索》

1. constructor

对于监听器 watch 而言,在业务代码中定义的时候 里面是一个对象{},因此 key 都为 string,所以const watcher = new Watcher(vm, expOrFn, cb, options);里面 expOrFn 一直都为 string, cb 为函数,当某个 watch 为对象的时候 options 为这个对象,内部含有 immediate handler deep 属性,若为函数则 options 为 undefined

因此在Watcher的 constructor 中对 expOrFn 做兼容处理的逻辑会走到else内部

ts
// parse expression for getter
if (isFunction(expOrFn)) {
  this.getter = expOrFn;
} else {
  // watch Watcher走的是这里
  this.getter = parsePath(expOrFn); // 解析并根据正则校验,当前watch的key是否符合命名规范
  if (!this.getter) {
    this.getter = noop;
    __DEV__ &&
      warn(
        `Failed watching path: "${expOrFn}" ` +
          "Watcher only accepts simple dot-delimited paths. " +
          "For full control, use a function instead.",
        vm
      );
  }
}

this.getter = parsePath(expOrFn)是对watch的 key 命名做校验的,校验通过则为 key 创建并返回一个 getter 函数, 其实就是当前 key 在 vm 中的定义,为依赖收集做准备

parsePath 方法位于src/core/util/lang.ts,源码如下

ts
const unicodeRegExp =
  /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;

/**
 * Parse simple path.
 */
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`);
export function parsePath(path: string): any {
  if (bailRE.test(path)) {
    return;
  }
  const segments = path.split(".");
  // 为当前的watch创建一个getter函数,用于后续依赖收集
  return function (obj) {
    // 此函数可以看做是getter函数, obj就是后面会传入的vm
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return;
      obj = obj[segments[i]]; // 这里obj === vm
    }
    return obj;
  };
}

::: unicodeRegExp.source 解释 RegExp.prototype.source

source 属性返回一个值为当前正则表达式对象的模式文本的字符串,该字符串不会包含正则字面量两边的斜杠以及任何的标志字符。 :::

源码中是用.对属性进行拆分的, 这也是为啥在 watch 中可以监听xxx.xx.xxxx这样的属性的原因,vue 最终或将其变成 vm.xxx.xx.xxxx这样的形式

上述源码的结果若是能正确运用,会为当前 watch 中的某个 path 即(expOrFn)创建一个 getter 函数并返回, 这个函数会在后续过程中被 vm 调用且传入 vm 作为参数,目的是为了收集依赖.

这个 getter 函数内部的本质就是取到真正要监听的属性在 vm 对象中目标层级上的函数或属性定义, 调用它就等价于在调用 vm 身上某个属性的 getter 进行取值的过程

例如:

js
export default {
  data() {
    return {
      pageUrl: ""
    };
  },
  watch: {
    pageUrl(val, oldVal) {
      /* .......... */
    },
    '$route.path':{
      handler()
    }
  }
};

上述代码中的:

  • key 为 pageUrl 的监听器创造了一个 getter, 这个 getter 的内部其实就是执行 vm.pageUrl 的过程
  • key 为 $route.path 的监听器也创造的一个 getter,这个 getter 的内部是执行 vm.$route.path 的过程

回到consturctor源码中,得到 key 的 getter 函数后,将其复制给 Watcher 的实例,然后做了一步取值操作,且利用取值的操作完成了 watch 依赖收集

js
this.value = this.lazy ? undefined : this.get();

由于监听器 Watcher 没有 lazy 属性, 所以会直接走到 this.get() 的调用过程,get 函数就是依赖收集的过程,下面继续分析

2. get 依赖收集

constructor的最后,调用this.get()方法,get 方法的源码如下:

ts
/**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm); // this.getter内部就是 vm.xxx, 因为vm.xxx会触发xxx的getter
    } 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) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }

首先,当前的 watcher 实例 push 进入targetStacktargetStack是在 Dep 里面定义的,接着从实例上拿到 vm,并用 vm 调用 getter 函数取值且,将 vm 作为参数传进去

ts
pushTarget(this);
const vm = this.vm;
value = this.getter.call(vm, vm);

上一步已经分析出来了, 这里的this.getter函数就是上一步constructor里面parsePath方法得到并挂到this上面的,并且知道在调用这个this.getter函数其实就是在执行vm.xxx, 那么在initWatch之前, vue 已经完成了 initProps initMathods initData, 并且已经完成了将propsdata内的属性用Object.defineProperty进行拦截, 所以此处在调用this.getter函数进行属性取值的时候, 其实就是为了利用对 watch 的 key 属性进行 vm 取值的这个过程,触发这个 key 属性的 getter

这里贴一段 Object.defineProperty 中 getter 的源码,便于分析

ts
// Object.defineProperty 的get函数定义, 源码位置: src/core/observer/index.ts

get: function reactiveGetter() {
  const value = getter ? getter.call(obj) : val;
  // 由于上面 已经 pushTarget(this); 因此这里为true
  if (Dep.target) {
    if (__DEV__) {
      dep.depend({
        target: obj,
        type: TrackOpTypes.GET,
        key
      });
    } else {
      dep.depend();
    }
    if (childOb) {
      childOb.dep.depend();
      if (isArray(value)) {
        dependArray(value);
      }
    }
  }
  return isRef(value) && !shallow ? value.value : value;
}

看到除了正常取值外就是依赖收集的逻辑

取值就不说了, 取完值后用 value 变量存起来后, 判断了 Dep.target 是否有值, target 是 Dep 类的静态属性, 上一步的pushTarget就是为其赋值的,因此这时候汇金到if里面

ts
// Dep

Dep.target = null;
const targetStack: Array<DepTarget | null | undefined> = [];

export function pushTarget(target?: DepTarget | null) {
  targetStack.push(target);
  Dep.target = target;
}

if里面首先执行的是 dep.depend(),然后检查有 childOb,childOb 表示 children 有被观察过,则也调用子级的 depend 方法,若 vm.xxx 为数组,则递执行前面逻辑

Dep.property.depend 方法源码如下:

ts
// Dep实例方法
depend(info?: DebuggerEventExtraInfo) {
  if (Dep.target) {
    Dep.target.addDep(this)
    if (__DEV__ && info && Dep.target.onTrack) {
      Dep.target.onTrack({
        effect: Dep.target,
        ...info
      })
    }
  }
}

同样, 这里面的 Dep.target 是有值的, 且值就是当前这个侦听器的一个watcher实例

depend 内部就仅做了一件事, 调用了Watcher原型上的addDep,并将dep实例作为参数传入

js
// Dep实例方法depend内部

// Dep.target为Watcher实例
if (Dep.target) {
  Dep.target.addDep(this); // this为dep, 因为这代码是在Dep的实例方法depend中执行的
}
  • Dep.targetWatcher实例
  • this为 Dep 实例dep, 因为这代码是在Dep的实例方法depend中执行的

接下来在来看一下Dep.target.addDep(this)也就是 watcher.addDep(dep)的源码:

ts
// Watcher实例方法: /src/core/observer/watcher.ts
/**
 * Add a dependency to this directive.
 */
addDep(dep: Dep) {
  const id = dep.id // 每一个dep实例都有的一个唯一的标识
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}

addDep的源码中会先检测当前watchernewDepIds中是否包含传入的 dep 的 id,若存在则什么也不做,不存在才会接着往下执行,是如何判断的呢?

首先,每一个 Dep 的实例上都会有一个唯一的 id 属性,在new Dep的时候,会进行 id 的累加

ts
let uid = 0;

class Dep {
  constructor() {
    this.id = uid++;
  }
}

其次,每一个监听器 watch 中的每一项属性 都会对应一个或者多个 Watcher 实例watcher, 且每一个watcher自身上都会有一个newDepIds和一个depIds,他们都是new Set(),还有一个depsnewDeps,他们是数组[],每一项,为何要这样设计呢?

前面说过, watch 的值可以是一个数组,那么一个key可以对应多个处理函数,每一个处理函数最终都会对应一个Watcher实例,当被监听的 key 变化后,会执行这一些处理函数,

同样,每一个 watcher 都有一个唯一的 id,原理同dep.id一样