依赖收集

我们调用defineReactive给响应式属性添加了get特性,get函数将在该属性被访问时调用并将返回作为属性值。

get特性函数执行时,第一步是先计算出响应式属性的值。之后,就是收集依赖的过程。

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

  const getter = property && property.get

  // 递归地对 val 进行响应式处理,并返回 val 对应的 __ob__
  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) {
        // 1、依赖收集:
        //   - 该属性值的闭包 dep 将当前 Dep.target 作为订阅者
        //   - 当前 Dep.target 将该属性值的闭包 dep 作为依赖
        // 以便该属性值自身变化时,通知订阅者
        dep.depend()
        if (childOb) {
          // 2、子属性的依赖收集(仅当该属性值为对象时):
          //   - 该属性值对应的观察对象的属性 dep 将当前 Dep.target 作为订阅者
          //   - 当前 Dep.target 将该属性值对应的观察对象的属性 dep 作为依赖
          // 以便该属性值动态增加/删除 属性/元素 的时候通知 watcher
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 3、若该属性值是数组,还需递归针对数组每个元素进行子属性的依赖收集
            dependArray(value)
          }
        }
      }
      return value
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

属性的依赖收集

我们知道,在 Watcher 计算其表达式时,会将当前Dep.target设置为该Watcher。若是 Watcher 在计算表达式的过程中访问了响应式属性,那么就会在此时做依赖收集的工作。

dep是响应式属性的闭包dep,调用dep.depend(),进而调用了Dep.target.addDep()方法将dep添加到了Dep.targetnewDeps里,这样dep就成为了Dep.target这个 Watcher 的依赖了。与此同时,dep也会将Dep.target这个 Watcher 添加到dep.subs,这样Dep.target就成为了dep的订阅者了。

// src/core/observer/dep.js
export default class Dep {
  // ...
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  // ...
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/core/observer/watcher.js
export default 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)) {
        // 若是该 watcher 之前没有过该 dep,则将 watcher 添加到 dep.subs(订阅者) 里
        dep.addSub(this)
      }
    }
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

属性的值的依赖收集

需要注意的是,此处的闭包dep所关联的是响应式属性自身,也就意味着只有当属性值整个被替换时,才会去通知订阅者。但若是属性值是引用类型比如对象,给对象添加/删除属性,对象的引用并没有改变,此时无法触发dep.notify()来通知订阅者,那该怎么办呢?(可先阅读下一节通知更新,再回来阅读下面的内容)

let childOb = !shallow && observe(val)
1

注意到,我们在给响应式属性添加getset之前,执行了上面这一句,而这就是响应式属性值处理为响应式对象(若属性值是对象或数组的话)并返回了属性值的观察者对象childOb,而观察者对象的childOb.dep也是跟闭包dep一样的依赖对象。紧接着,调用childOb.dep.depend将当前Dep.targetchildOb.dep关联起来,Dep.target成为了childOb.dep的订阅者,childOb.dep也成为了Dep.target的依赖。若响应式属性的值为数组,还会调用dependArray(value)以遍历数组每个元素来收集依赖。最终在响应式属性值内部的子属性或元素发生变化时,也能通知到订阅者了(详情请见通知更新)。

function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
1
2
3
4
5
6
7
8
9

总结

Watcher 在计算表达式的值的时候,会将响应式属性的闭包dep作为依赖。若响应式属性的值是引用类型,还会将响应式属性的值对应的观察者对象的dep作为依赖。这样的话,无论是响应式属性改变,还是响应式属性值的子元素/子属性改变,都能调用不同的dep.notify通知到订阅者进行更新。