# 依赖收集

依赖收集收集的是 WatcherWacher 是观察者,用于订阅数据的变化,当响应式数据变化的时候,会触发对应的 setter

# Dep

Vue.js关于依赖收集相关的类是Dep,位置在core/observer/dep.js,主要收集的方法为dep.depend,调用的地方在defineReactive中的getter,这就是依赖收集的地方。

export default class Dep {
  // 静态属性,watcher对象
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor() {
    this.id = uid++;
    this.subs = [];
  }

  addSub(sub: Watcher) {
    this.subs.push(sub);
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub);
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  notify() {
    const subs = this.subs.slice();
    if (process.env.NODE_ENV !== "production" && !config.async) {
      subs.sort((a, b) => a.id - b.id);
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
  • Dep是一个类,他有一个静态属性target,Dep.target用来存储当前的Watcher,subs用来存储各种Watcher,包括render watcher,computed watcher,user watcher
  • depend就是收集依赖
  • notify,当数据变化的时候,会通知dep.notify(),这个时候就会执行这个方法,最终执行watcher.update(),这个属于派发更新。
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建依赖对象,一个对象的属性对应一个依赖
  const dep = new Dep();

  // 是否需要递归观察对象的值
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      // 收集依赖
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      // 返回get属性对应的值
      return value;
    },
  });
}

new ObserverdefineReactive的时候会进行new Dep,我们通过代码来看下一共有几个 dep 实例:

export default {
  data() {
    return {
      obj: {
        msg: "hello",
      },
    };
  },
};

一开始会初始化整个 data,然后 observe(data),会执行 new Observer 实例化,这个时候有了第一个 dep

//  {id:0,subs:[]}
const dep = new Dep();

之后会对整个对象进行 walk,循环进行 defineReactive({obj:{msg:'hello'}},'obj'),这时候会有第二次的 dep

//  {id:1,subs:[]}
const dep = new Dep();

defineReactive中,又会对 childObj 进行 observe,由于{msg:'hello'}也是对象,所以又会重新 new Observer

//  {id:2,subs:[]}
const dep = new Dep();

之后会对整个对象进行 walk,循环进行 defineReactive({msg:'hello'},'msg'),这时候会有第四次的 dep

//  {id:3,subs:[]}
const dep = new Dep();

所以总共会进行 4 次的 dep 实例化分别是一开始根对象dep,两次defineReactive函数中的depchildObj中的dep,这里要注意的是,默认有两个 dep 实例'$attrs','$listeners'。

# Dep.target

当初次渲染的时候,会调用new Watcher(),最终执行this.get(),在get中,会pushTarget(this),这里的this就是当前的Watcher对象

Watcher下面的get函数:

  get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }

pushTarget定义在Dep类内,很明显的是Dep.target就是存储的Watcher对象

Dep.target = null;
const targetStack = [];

export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}
export function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

# Watcher

Watcher也是一个类,定义在core/observer/watcher.js

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  invokeWithErrorHandling,
  noop,
} from "../util/index";

import { traverse } from "./traverse";
import { queueWatcher } from "./scheduler";
import Dep, { pushTarget, popTarget } from "./dep";

import type { SimpleSet } from "../util/index";

let uid = 0;

export default class Watcher {
  vm: Component;
  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;
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
    }
    // computed watcher lazy = true
    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) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      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);
      }
    }
  }

  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp = 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;
  }

  update() {
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }

  run() {
    if (this.active) {
      const value = this.get();
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        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() {
    this.value = this.get();
    this.dirty = false;
  }

  depend() {
    let i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  }

  teardown() {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this);
      }
      let i = this.deps.length;
      while (i--) {
        this.deps[i].removeSub(this);
      }
      this.active = false;
    }
  }
}

可以很明显的看到new Watcher最后会执行this.get(),之后会将Dep.target设置为当前的Watcher,执行getter函数,这个函数就是我们渲染时候定义的函数。最后调用popTarget(), 会把targetStack最后一个移除,同时设置Dep.target为倒数第二个。

这里用了一个栈的数据结构,是为了处理父子组件的关系, 父子组件嵌套的时候把父组件的 watcher 先入栈,再去处理子组件的 watcher,子组件处理完毕后,在把父组件 watcher 出栈,子组件渲染完毕后,才会父组件才算渲染完毕,也就是child mounted -> parent mounted,这样可以保证组件在渲染的过程中保持正确的依赖。

# dep.depend

defineReactive中的getter会进行依赖收集dep.depend()

// observer/index.js
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val;
    // 收集依赖
    if (Dep.target) {
      dep.depend();
      if (childOb) {
        childOb.dep.depend();
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }
    // 返回get属性对应的值
    return value;
  },
});

// Dep.js
class Dep {
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
}

Dep定义可以看到,有Dep.target就是Watcher实例,也就是会调用addDep方法,同时传入Dep实例

class Watcher {
  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);
      }
    }
  }
}

这里首先会获取 dep 实例的 id,然后判断是否在新的依赖中存在 id,如果没有,将 depId 存入 newDepIds,将 dep 存入 newDeps,在判断当前的 id 是否在旧的 depIds,如果没有,则调用 dep.addSub 方法,将当前的 watcher传入,也就是当前的dep实例下的subs有这个watcher实例。

这里主要是为处理同一个依赖的情况:

<template>
  <div>
    {{ age }}
    {{ age }}
  </div>
</template>

当模版页面渲染的时候,render 函数遇到 age 变量,就会触发 getter 进行依赖收集,将 age 所处的 dep 实例存入 newDepIds,第二次遇到 age 的时候,又回触发 getter,但是由于已经在 newDepIds 存在,就直接跳过。

在收集依赖的逻辑中,还有一个逻辑:

if (childOb) {
  // 2. 如果属性对应的值是对象或者数组,也需要收集依赖来处理数组或者对象的变化 this.name.push(1)
  childOb.dep.depend();
  // 3. 如果属性的值是数组,又会重新判断数组内的每一个值是否为数组或对象,如果是,又会重新收集依赖 this.name = [1,[2,3],4]
  if (Array.isArray(value)) {
    dependArray(value);
  }
}

收集依赖一共有三种情况需要考虑:

export default {
  data() {
    return {
      name: [1, 2, {}, [1, 2]],
    };
  },
};
  • 属性: 每一个属性都是一个dep,所以需要dep.depend(),比如this.name = '1',发生变化。
  • 属性值: 如果属性值是对象或者数组,也需要收集依赖,比如this.name.push(1)
  • 属性值如果是数组: 还需要判断值是否为响应式对象,如果是也需要收集依赖this.name = [1,2,{a:1,__ob__:__ob__}]。最后如果数组的值还是数组,需要递归处理。

# cleanupDeps

当我们执行完渲染函数的最后,会进行cleanupDeps对依赖进行清除

class Watcher {
  // 精简代码
  constructor() {
    this.deps = []; // 旧dep
    this.newDeps = []; // 新dep
    this.depIds = new Set(); // 旧dep id
    this.newDepIds = new Set(); // 新dep id
  }
  cleanupDeps() {
    let i = this.deps.length;
    while (i--) {
      const dep = this.deps[i];
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this);
      }
    }
    let tmp = 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;
  }
}

假设我们的代码:

<div id="app">
  <p v-if="a===1">{{b}}</p>
  <button @click="change" type="button">click me</button>
</div>
<script>
  new Vue({
    el: "#app",
    data: {
      a: "a",
      b: "b",
    },
    methods: {
      change() {
        this.a = 1;
      },
    },
  });
</script>

首次渲染的时候,只会触发 agetter,所以:

this.deps = [];
this.newDeps = [{ id: 3, subs: [Watcher] }];

渲染完成之后执行 cleanupDeps,会将 newDepid 和内容都放入 deps,然后清空自身:

this.deps = [{ id: 3, subs: [Watcher] }];
this.newDeps = [];

当我们执行按钮的时候,会触发两个 getter,分别是 ab,其中 a 的值为 1,b 的值为'b',首先是 a,由于 adep 之前已经存入了 deps,所以不会执行 dep.addSub,所以这个时候:

this.deps = [{ id: 3, subs: [Watcher] }];
this.newDeps = [
  { id: 3, subs: [Watcher] },
  { id: 4, subs: [Watcher] }, // 4 就是b的getter初始化的dep实例
];

最后同样会执行 cleanupDeps:

this.deps = [
  { id: 3, subs: [Watcher] },
  { id: 4, subs: [Watcher] },
];
this.newDeps = [];

这样的好处是可以避免无关的依赖重复渲染组件,执行回掉函数等。