【Vue源码】15.响应式原理-依赖收集

351 阅读5分钟

响应式原理-依赖收集

在上一节 14.响应式原理-响应式对象 中,我们知道了 Vue 会把普通对象转化为响应式对象,在响应式对象中,getter 的相关逻辑就是做依赖收集的。 由于依赖收集不是特别好理解,代码逻辑绕老绕去,所以本节前半段大概介绍一下涉及到的代码片段,后半部分通过分析依赖收集的过程再了解详细的内容。

我们先回顾一下上节内容:

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  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);
          }
        }
      }
      return value;
    },
    // ...
  });
}

这段代码主要逻辑:

  1. const dep = new Dep() 实例化一个 Dep 实例
  2. get 中通过 dep.depend 做依赖收集

Dep

Dep 是依赖收集的核心,定义在 src/core/observer/dep.js 中:

import type Watcher from "./watcher";
import { remove } from "../util/index";

let uid = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
// 当响应式数据读取时,收集依赖
export default class Dep {
  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() {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 全局唯一的 Watcher,在同一时间只能有一个全局的 Watcher 被计算
Dep.target = null;
const targetStack = [];

export function pushTarget(_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = _target;
}

export function popTarget() {
  Dep.target = targetStack.pop();
}

DepClass,定义了一些属性和方法,静态属性 target 被赋值为全局唯一的 Watcher,意为在同一时间只能有一个全局的 Watcher,同时 subs 属性也是 Watcher 数组。

DepWatcher 是紧密联系在一起的,是对 Watcher 的一种管理,在这里脱离 Watcher 的话 Dep 是无存在意义的。

Watcher

let uid = 0;

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  dep: Dep;
  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.computed = !!options.computed;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.computed = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid; // uid for batching
    this.active = true;
    this.dirty = this.computed; // for computed watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new Set();
    this.newDepIds = new Set();
    this.expression =
      process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
    // parse expression for getter
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = function () {};
        process.env.NODE_ENV !== "production" &&
          warn(
            `Failed watching path: "${expOrFn}" ` +
              "Watcher only accepts simple dot-delimited paths. " +
              "For full control, use a function instead.",
            vm
          );
      }
    }
    if (this.computed) {
      this.value = undefined;
      this.dep = new Dep();
    } else {
      this.value = 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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      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);
      }
    }
  }

  /**
   * 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 = 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;
  }
  // ...
}

Watcher 也是 Class,在构造函数中定义了一些和 Dep 相关的属性:

this.deps = []; // 表示 Watcher 实例持有的 Dep 实例的数组
this.newDeps = []; // 表示 Watcher 实例持有的 Dep 实例的数组
this.depIds = new Set(); // 代表上面数组的 id
this.newDepIds = new Set(); // 代表上面数组的 id

这些属性表示 Watcher 持有的 Dep 实例的数组,这里为什么会用两个数组等会再看。 Watcher 还定义了一些原型方法,和依赖收集相关的有 getaddDepcleanupDeps。等会分析过程的时候再具体介绍这几个方法。

过程分析

在分析之前先大概看一下流程图,心里有个印象:

15.响应式原理-依赖收集.png

首先来看是在什么时候什么地方触发的 get。 在我们之前介绍 Vuemount 过程是通过 mountComponent 函数,其中有段逻辑:

updateComponent = () => {
  vm._update(vm._render(), hydrating);
};
new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if (vm._isMounted) {
        callHook(vm, "beforeUpdate");
      }
    },
  },
  true /* isRenderWatcher */
);

在这段代码中,Vue 去实例化一个渲染 Watcher 时,第二个参数就是 updateComponent,继续进入 Watcher 的构造函数逻辑,然后执行 this.get() 方法,在 get 方法中,首先会执行:

pushTarget(this);

pushTarget 定义在 src/core/observer/dep.js 中:

export function pushTarget(_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = _target;
}

它的作用就是把当前 Watcher 赋值给 Dep.target,并且把之前的 Dep.target 存在栈中,方便后续取出来。栈的出入顺序是先入后出,有没有联想到我们之前讲数据驱动时父子组件的插入顺序也是先入后出,父组件先执行然后子组件执行,子组件执行完出来后父组件再执行。

回到 get 方法,执行完 pushTarget(this) 之后接着又执行了:

value = this.getter.call(vm, vm);

this.getter 对应的就是 Watcher 的第二个参数,也就是 updateComponent

vm._update(vm._render(), hydrating);

他会执行 vm._render() 方法,这个我们之前分析过 4._render,他返回一个渲染 VNode,并且在这个过程中会对 vm 上的数据进行访问,在这个时候就触发了数据对象的 getter

在每个对象值的 getter 方法中,都有一个 dep,被触发时会调用 dep.depend() 方法:

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

在这里 Dep.target 就是在 new Watcher 执行 get 方法时存入的当前 Watcher 实例。所以相当于执行 Watcher.addDep 方法:

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.addSub(this)

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

在这里就把当前 Watcher 保存到当前 Watcher 持有的 depsubs 中,目的是为了后续数据变化时通知哪些 subs 做准备。

让我们返回 Watcher,在执行完 value = this.getter.call(vm, vm) 后,会执行:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (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
}

traverse 会递归访问 value,触发所有子项的 getter,接下来执行 popTarget

export function popTarget() {
  Dep.target = targetStack.pop();
}

在这里会把 Dep.target 恢复成上一个 Watcher,因为当前 vm 的数据依赖已经收集完毕。 最后再执行 this.cleanupDeps()

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
}

这个函数的主要目的是清除依赖收集。 为什么要清除? 因为 Vue 是数据驱动,每次数据变化都会重新 rendervm._render 方法就会重走一遍,再次触发数据的 getter,所以在 Watcher 的构造函数中会初始化两个 Dep 实例数组,newDeps 表示新添加的 Dep 实例数组,deps 表示上一次添加的。 在执行 cleanupDeps 时,会首先遍历 deps,移除对 dep.subs 的订阅,然后把 newDepIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。 那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。 考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 ab,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理。

总结

15.响应式原理-依赖收集.png

在这一节中,我们分析了依赖收集的过程。 依赖收集的目的是为了当这些响应式数据变化触发 setter 时,能知道该通知哪些订阅者去执行相关的逻辑。这个过程叫做派发更新,其实 WatcherDep 是典型的观察者设计模式,下一我们我们来详细的分析派发更新的过程。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘