依赖收集、派发更新和Vue.set

defineReactive给数据添加了 getter 和 setter。
在defineReactive内第一步是实例了Dep

Dep是整个 getter 依赖收集的核⼼

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array;
  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()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

target 是⼀个全局唯⼀ Watcher ,这是⼀个⾮常巧妙的设计,因为在同⼀时间只能有⼀个全局的 Watcher 被计算,另外它的⾃⾝属性 subs 也是 Watcher 的数组。

Watcher

这个Class的构造函数中定义了一些和Dep相关的属性。

  • this.deps 和 this.newDeps 表⽰ Watcher 实例持有的 Dep 实例的数组;
  • this.depIds 和 this.newDepIds 分别代表 this.deps 和 this.newDeps 的 id Set。
  • Watcher还定义了⼀些原型的⽅法,和依赖收集相关的有 get 、 addDep 和 cleanupDeps⽅法
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

依赖收集

  //defineReactive方法内部
  let childOb = !shallow && observe(val)//可能还是个obj
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()// 执行Watcher ? Dep.target.addDep(this)
        if (childOb) {
          childOb.dep.depend()
          //收集了依赖
          //执⾏ Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher
          //从⽽让添加新的属性到对象也可以检测到变化
          if (Array.isArray(value)) {
            dependArray(value)//把数组每个元素也去做依赖收集
          }
        }
      }
      return value
    }
    //...
  })

在mount过程中,mountComponent函数有这么一段逻辑

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

  • 当我们去实例化⼀个渲染 watcher 的时候,⾸先进⼊ watcher 的构造函数逻辑,实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复⽤)
  • 接着调用vm._update,因为这个方法会生成渲染VNode,并且会对vm进行数据访问,因此会触发数据对象的getter,而每个getter都持有一个dep
  • 在触发getter的时候会调用dep.depend(),即执行Dep.target.addDep(this),在保证不会被重复添加的情况下,把当前的 watcher 订阅到这个数据持有的 dep 的 subs,this.subs.push(sub),⽬的是为后续数据变化时候能通知到哪些 subs 做准备。
  • traverse(value)递归触发子项getter
  • Dep.target = targetStack.pop(),把Dep.target恢复成上一个状态
  • this.cleanupDeps(),设计了在每次添加完新的订阅,会移除掉旧的订阅。(比如v-if渲染不同子模板a b,渲染a改a通知a的subs,切换渲染b去修改a,若会通知a的subs的回调就是浪费,因此要移除旧的订阅)

派发更新

setter 的逻辑有 2 个关键的点,⼀个是 childOb = !shallow && observe(newVal) ,如果 shallow为 false 的情况,会对新设置的值变成⼀个响应式对象;另⼀个是 dep.notify() ,通知所有的订阅者

  //defineReactive方法内部
  let childOb = !shallow && observe(val)//可能还是个obj
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
    //...
  })

在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最终调用dep.notify(),即遍历subs调用每个watcher的update方法,会对watcher不同状态走不同逻辑,一般情况下会执行queueWatcher逻辑

  • 引入一个队列的概念,因为每次数据改变都触发watcher回调,把这些watcher添加到一个队列里,然后异步执行nextTick(flushSchedulerQueue)
    • 把传⼊的 cb 压⼊callbacks 数组,最后⼀次性在下一个tick执⾏
    • 因此数据的变化到 DOM 的重新渲染是⼀个异步过程,发⽣在下⼀个 tick。
  • flushSchedulerQueue会把queue队列做一个id从小到大的排序,原因如下
    • 1.组件的更新由⽗到⼦;因为⽗组件的创建过程是先于⼦的,所以 watcher 的创建也是先⽗后⼦,执⾏顺序也应该保持先⽗后⼦
    • 2.⽤户的⾃定义 watcher 要优先于渲染 watcher 执⾏;因为⽤户⾃定义 watcher 是在渲染watcher 之前创建的
    • 3.如果⼀个组件在⽗组件的 watcher 执⾏期间被销毁,那么它对应的 watcher 执⾏都可以被跳过,所以⽗组件的 watcher 应该先执⾏
  • 在排序后遍历拿到对应watcher,执行watcher.run()。在遍历的时候每次都会对queue.length求值,因为在run()的时候queue可能会发生变化
    • run函数传入watcher的回调函数,先this.get()求值,做判断满足条件执行watcher回调,并且传入newVal和oldVal
    • this.get()求值的时候会执行getter,触发组件重新渲染
  • 状态恢复,把流程控制变量设回初始值,把watcher队列情况。

针对特殊情况

给响应式对象添加新属性,使用Vue.set API

set ⽅法接收 3个参数, target 可能是数组或者是普通对象, key 代表的是数组的下标或者是对象的键值, val 代表添加的值。

  • ⾸先判断如果 target 是数组且 key 是⼀个合法的下标,则通过 splice 去添加进数组然后返回(splice并非原生splice)
  • 若key 已经存在于 target 中,则直接赋值返回
  • 获取target.__ob__(Observer的实例),若不存在表明target飞响应式对象直接返回
  • 通过defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() ⼿动的触发依赖通知。(因为在getter过程中有childOb的判断并且调用childOb.dep.depend()收集了依赖,因此能notify()通知)
  • 数组情况,在observe方法观察对象的时候对数组作了处理,对数组中所有能改变数组⾃⾝的⽅法,如push、pop 等这些⽅法进⾏重写。重写后的⽅法会先执⾏它们本⾝原有的逻辑,并对能增加数组⻓度的 3 个⽅法 push、unshift、splice ⽅法做了判断,获取到插⼊的值,然后把新添加的值变成⼀个响应式对象,并且再调⽤ ob.dep.notify() ⼿动触发依赖通知

你可能感兴趣的:(依赖收集、派发更新和Vue.set)