Vue2源码学习笔记之观察者

下面看下观察者 Watcher 的代码,这个类比较复杂,里面创建了很多属性和方法。

目录
  1. Watcher观察者
  2. 收集依赖
  3. 触发依赖
  4. 参考资料

Watcher观察者#

下面看下观察者Watcher的代码,这个类比较复杂,里面创建了很多属性和方法。

export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
  }
 
  get() {
    // ...
  }
 
  addDep(dep: Dep) {
    // ...
  }
 
  cleanupDeps() {
    // ...
  }
 
  update() {
    // ...
  }
 
  run() {
    // ...
  }
 
  getAndInvoke(cb: Function) {
    // ...
  }
 
  evaluate() {
    // ...
  }
 
  depend() {
    // ...
  }
 
  teardown() {
    // ...
  }
}

看下类的构造函数。

export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // ...
  }
}

构造函数有五个参数,分别是:

  1. Vue 的实例对象vm
  2. 要监听的目标expOrFn,即可以是字符串,也可以是一个返回目标的函数。
  3. 监听目标变化时的回调函数cb
  4. 传递给当前观察者对象的选项options
  5. 是不是渲染函数观察者isRenderWatcher

现在重新回头查看mountComponent函数中生成Watcher实例对象的代码。

// ! 生成观察者实例
new Watcher(
  vm,
  updateComponent, // ! 更新组件的函数,执行时 => 执行 vm.$options.render => getter => 收集依赖
  noop,
  {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, "beforeUpdate"); // ! 传入 beforeUpdate 生命周期钩子函数,在更新前组件前调用
      }
    },
  },
  true /* isRenderWatcher */
);

第一个参数vm是当前需要挂载的实例对象。第二个参数updateComponent是监听的目标,它是一个生成真实元素的函数。第三个参数noop是一个空函数。第四个参数options是选项对象,这里传入了一个before函数,这个函数调用了beforeUpdate生命周期钩子函数。第五个参数isRenderWatcher是一个布尔值true,说明这是一个渲染函数的观察者。

其中,第二个参数expOrFn会把它变成一个getter。而第三个参数cb是一个空函数,什么也没做。因为这是一个渲染函数的观察者,它只执行getter收集依赖,不用去触发依赖。

下面继续看构造函数的代码。

this.vm = vm;
if (isRenderWatcher) {
  vm._watcher = this;
}
vm._watchers.push(this); // ! 把自己添加进去

把需要挂载的实例对象vm存储到观察者的vm属性中,这样每个观察者的vm都存储着它所属的实例对象,这表明了这个观察者的归属。

判断传入的第五个参数isRenderWatcher是否为true,如果是的话,就把当前观察者存储到实例对象的_watcher属性中。

把观察者放入到实例对象的_watchers属性中,这个属性存储所有的观察者实例,不管是渲染函数观察者还是其他的观察者。

注意实例对象中的_watcher属性在初始化时通过initLifecycle函数定义过,它的初始值是null。而_watchers属性是通过initState函数定义过,它的初始值是空数组。

继续看下面的代码。

// options
if (options) {
  this.deep = !!options.deep; // ! 是否深度监听
  this.user = !!options.user; // ! 是否是开发者定义的,主要指 watch $watch 中自定义的函数
  this.lazy = !!options.lazy; // ! 是否开启懒惰模式,专为计算属性设置
  this.sync = !!options.sync; // ! 是否同步求值
  this.before = options.before; // ! 更新之前调用的钩子函数,比如调用 beforeUpdate 钩子函数
} else {
  this.deep = this.user = this.lazy = this.sync = false; // ! 默认值为 false
}

通过传入的第四个参数options选项来初始化观察者的一些属性,其中deepuserlazysync属性的值是布尔类型,如果没有传入值,它们的值默认为undefined,然后通过双重取反变成布尔值false

下面来分析这些属性:

  • deep:用来告诉观察者当前的监听对象是否执行深度监听。
  • user:用来告诉观察者当前的对象是用户定义的还是内部定义的。除了内部定义的观察者,比如渲染函数观察者、计算属性观察者,其他的观察者一般都是用户定义的。
  • lazy:用来告诉观察者当前的监听对象是不是计算属性。
  • sync:用来告诉观察者当前的监听的对象变化时是否进行同步求值。
  • before:这是在监听的对象变化之前调用的生命周期钩子函数,即beforeUpdate钩子函数。

继续看下面的代码。

this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true; // ! 观察者默认是激活状态
this.dirty = this.lazy; // for lazy watchers

定义this.cb属性,它的值是传入的第三个参数cb,即回调函数。

定义this.id属性,它是观察者的唯一标识,初始值为0,然后后面创建的观察者的 id 是递增的。

定义this.active属性,它表示观察者是否是激活状态,默认值是true

定义this.dirty属性,它的值和this.lazy相同,也是用于计算属性。

继续看下面的代码。

this.deps = []; // ! 上一次添加的 Dep 实例数组
this.newDeps = []; // ! 当前添加的 Dep 实例数组
this.depIds = new Set(); // ! 上一次添加到 Dep 实例的 id 的集合,不能重复
this.newDepIds = new Set(); // ! 当前添加到 Dep 实例的 id 的集合,不能重复

上面定义的四个订阅器相关的属性是用来避免重复收集依赖和移除无用依赖的。这些属性分成两组,其中depsdepIds是一组,用于上一次收集的依赖。newDepsnewDepIds是一组,用于当前收集的依赖。

继续看下面的代码。

this.expression =
  process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";

定义this.expression属性,在非生产环境时,它的值是第二个参数的字符串转换值,而在生成环境时,它的值是空字符串,说明这个属性是在非生产环境中使用的。

继续看下面的代码。

// parse expression for getter
if (typeof expOrFn === "function") {
  this.getter = expOrFn;
} else {
  this.getter = parsePath(expOrFn); // ! 字符串转函数
  if (!this.getter) {
    this.getter = noop;
    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
      );
  }
}

判断传入的第二个参数expOrFn的类型,如果它是函数类型,把它赋值给观察者的getter属性。如果不是函数类型,应该是一个字符串类型,调用parsePath函数把它转换成一个函数并赋值给getter。如果转换成函数失败,观察者的getter属性赋值为一个空函数,在非生产环境中报错,提示Watcher只接受简单的点分割路径,如果想要完整监听,需要使用函数。

看下parsePath函数的代码。

export 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/;
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`);
export function parsePath(path: string): any {
  if (bailRE.test(path)) {
    return;
  }
  // ...
}

在函数之前声明一个正则表达式bailRE,这个正则表达式的含义是:

  • 不能是unicodeRegExp.source
  • 不能是.符或者$符或者_符。
  • 不能是\d,即不能是数字。

看这个函数前面的if语句,如果传入的参数path匹配到了上面的正则表达式,就直接返回。

也就是说如果需要继续后面的逻辑,就必须不能满足上面的正则表达式,也就是和上面的条件相反。即传入的path必须要是unicodeRegExp.source,或者是数字,或者有.符,或者有$符,或者有_符,只要满足其中的一项条件即可。

继续看下面的代码。

const segments = path.split(".");
return function (obj) {
  for (let i = 0; i < segments.length; i++) {
    if (!obj) return;
    obj = obj[segments[i]];
  }
  return obj;
};

使用.符来分割path,把分割后的数组存储在变量segments中。

然后返回一个函数,返回的函数里面遍历segments,并且访问传入的实例对象obj中存在于segments中的path对应的值,这相当于一个触发了访问的值的getter函数。

继续看构造函数最后的代码。

// ! 实例化最后,调用 get 获取值,并收集依赖
// ! 注意计算属性采用不同的处理方法
this.value = this.lazy ? undefined : this.get();

定义this.value属性,即获取的值。非计算属性观察者会调用this.get方法获取值。而计算属性观察者的值为undefined,也就是没有收集依赖,因为计算属性获取值的时机不一样,后面会说明。

收集依赖#

下面看下Watcher下面的get方法,这个方法的作用是获取观察对象的值和收集依赖。

get() {
  pushTarget(this) // ! Dep.target 赋值为观察者实例 ,并放入维护的 targetStack 中
  // ...
  popTarget()
  this.cleanupDeps()
}

在函数的前面先调用了pushTarget函数把观察者放入到收集依赖的栈中,然后后面又调用了popTarget函数把她从收集的栈中删除。

先看下这个两个函数的代码,在core/observer/dep.js文件中。

export default class Dep {
  // ...
}
Dep.target = null; // ! 初始化为 null
const targetStack = [];
 
// ! 把 target 放入栈中,并赋值 Dep.target
export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}
 
// ! 把 target 移出栈中,Dep.target 变成栈的最后一个元素 或者 undefined
export function popTarget() {
  targetStack.pop();
  Dep.target = targetStack[targetStack.length - 1];
}

pushTarget函数会把传入的观察者赋值给了订阅器的Dep.target属性中,并且存入一个targetStack数组类型的栈中。

popTarget函数会把前面加入栈的观察者对象移出栈,并把Dep.target的值变成现在栈中的最后一个元素。

再看get中的其他代码。

let value;
const vm = this.vm;
try {
  // ! 执行 getter 获取值
  // ! 相当于执行 updateComponent 函数 => 即执行 vm._update(vm._render(), hydrating)
  // ! 而执行 vm._render => 对 vm 上的数据访问 => 触发 getter => 收集依赖
  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
  // ! 如果是深度观察 => 用于 watch 选项
  if (this.deep) {
    traverse(value); // ! 递归去访问 value,触发它所有子项的 getter
  }
 
  // ! 把 Dep.target 恢复成上一个状态
  // ! 当前 vm 的数据依赖收集已经完成,那么对应的渲染 Dep.target 也需要改变
  popTarget();
  this.cleanupDeps(); // ! 每次求值后清空当前的依赖,避免重复收集
}
return value; // ! 渲染函数观察者的返回值永远都是 undefined

声明变量value,这个遍历用来存储调用getter函数获取到的值,也是最后的返回值,声明常量vm存储this.vm,也就是观察者所属的实例对象。

调用try...catch...finally语句执行getter函数,是为了更好的捕获错误。先看try代码块的代码,执行this.getter函数,并把函数返回的值赋值给了变量value。再获取值的过程中,会触发值的getter属性,getter是在defineReactive函数中通过Object.defineProperty定义的get属性,然后getter会收集依赖。

这里分析下依赖收集的过程

首先,通过new Watcher生成一个观察者实例对象,需要传入实例对象、观察的目标expOrFn以及回调函数cb

new Watcher({ vm, expOrFn, cb });

在生成观察者实例对象的过程中,会执行类Watcher的构造函数的代码,构造函数的最后代码会调用Watcherget方法。

constructor(/* ... */) {
  // ...
  this.value = this.get() // 获取值
}

get方法中会执行getter方法,而这个getter方法一般就是传入的第二个参数expOrFn,即观察目标的表达式。在执行getter方法之前,会调用pushTarget函数把Dep.target赋值为这个观察者,并键入到收集依赖的栈中,然后就是执行getter方法,这时会访问到expOrFn执行后的值。

this.getter = expOrFn;
pushTarget(this); // 赋值为 Dep.target 入栈
value = this.getter.call(vm, vm); // 访问值
 
// pushTarget
export function pushTarget(target: ?Watcher) {
  targetStack.push(target);
  Dep.target = target;
}

访问到expOrFn执行后的值时,会触发这个值的getter函数,getter函数会先判断Dep.target是否有值。因为前面已经赋值,所以会执行Dep实例对象的depend方法。

get: function reactiveGetter() {
  const value = getter ? getter.call(obj) : val;
  if (Dep.target) {
    dep.depend(); // ! 添加进订阅器,依赖收集
    // ...
  }
  return value;
}

Dep实例对象的depend方法中,又会调用Dep.target属性的addDep方法,也就是调用观察者的addDep方法,把Dep实例对象自身加入到观察者的dep中。

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

查看观察者的addDep方法,先判断newDepIds中是否存在这个dep,如果没有,会把dep添加到观察者的newDeps集合中去,这样可以避免重复收集依赖。

然后判断集合depIds中是否有这个depid,如果没有的话,说明观察者还没有加入到dep中,调用depaddSub方法把观察者加入到dep中,到这一步,依赖收集就已经完成。

// Watcher
addDep(dep: Dep) {
  const id = dep.id
  // ! 需要先判定 new 中是否已经收集,如果没有收集才收集,避免重复收集依赖
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep) // 放入到当前的 Dep 数组
    if (!this.depIds.has(id)) {
      dep.addSub(this) // 观察者对象放入到 subs 中
    }
  }
}
 
// Dep
addSub(sub: Watcher) {
  this.subs.push(sub)
}

再看下catch代码块的代码,这里主要是捕获可能出现的错误。

最后看下finally代码块的代码,如果设置了this.deeptrue,说明设置了深度监听。这时需要调用traverse函数递归去访问value,触发它所有子项的getter,收集观察目标的所有依赖。

依赖收集完毕,还需要调用this.cleanupDeps清空当前的依赖,避免重复收集依赖。这里清空的是newDepsnewDepIds中的数据,它们表示当前收集到的Dep实例对象的集合。

最后返回获取到的值value

下面看下观察者的cleanupDeps方法,这个方法是清空当前收集到的Dep实例对象的集合,用来防止重复收集依赖。

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 // ! 交换 ids
  this.newDepIds = tmp
  this.newDepIds.clear() // ! 清空 newDepIds
  tmp = this.deps
  this.deps = this.newDeps // ! 交换 deps
  this.newDeps = tmp
  this.newDeps.length = 0 // ! 清空 newDeps
}

一开始是对deps进行遍历,即对上一次求值所收集到的dep进行遍历,在循环的过程中,检查上一次求值所收集到的dep是否存在于当前收集到的deps中,如果不存在,说明该dep已经和观察者不存在依赖关系,这时调用dep.removeSub方法将观察者从depsubs中移除。

depremoveSub方法的代码如下。

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

然后在每次收集依赖后,会清空当前收集到的dep对象,即清空newDepIdsnewDeps中的值,在清空之前先把它们的值赋值给depIdsdeps

这样,newDepIdsnewDeps所存储的值总是当前收集到的dep,而depIdsdeps存储的值总是上一次收集到的dep

触发依赖#

在我们修改响应式数据时,会执行它的setter,我们看下defineReactive函数的定义的setter是如何触发依赖的。

set: function reactiveSetter(newVal) {
  // ...
  dep.notify(); // ! 通知观察者, 触发依赖
}

setter中最后会调用depnotify方法通知所有观察者(依赖)。

看下notify方法的代码,在Dep类中。

notify() {
  // ...
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}

调用了所有观察者的update方法,这个方法是用来更新值,更新视图的。

看下update方法的代码,在Watcher类中。

update() {
  /* istanbul ignore else */
  // ! 如果启动懒惰模式  => 更新 dirty 为 true,说明可以更新计算属性的值
  if (this.lazy) {
    this.dirty = true
 
    // ! 同步执行,不需要调用 nextTick 执行观察者 => 用于 watch 自定义的观察者
  } else if (this.sync) {
    this.run()
 
    // ! 异步执行,需要调用 nextTick 执行观察者 => 用于执行自动收集的依赖
  } else {
    queueWatcher(this) // ! 使用异步队列更新
  }
}

先判断this.lazy属性是否为真,如果为真,设置this.dirty属性为true。这样使观察者开启懒惰模式,也说明这个观察者是计算属性观察者。

this.dirty设置为true,表示它的值发生了变化,观察者可以更新它的值。这个属性用来设置计算属性观察者是否可以执行更新。计算属性观察者的更新以及如何收集依赖在后面计算属性时再学习。

下面就是观察者同步执行和异步执行的代码。

参考资料#