Vue依赖收集

205 阅读1分钟

Vue Dep Collection

Created: August 17, 2022 12:52 AM

Relationship in short:

  1. One Watcher - One Vue Component
  2. One Observer - One Object in data option
  3. One Dep - One key-value in a object, also a Observer

关键概念:Vue 每次只更新一个组件,换言之全局只会同时更新一个 Watcher,也就是 Dep.target 全局对象

export default class Dep {
  static target: ?Watcher

  // ...

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

  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep.target = null
const targetStack = []

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

在每个 Vue 组件 beforeMount 和 mount 钩子之间,Vue 为每个组件创建一个属于自己的 Watcher (也就是 vm._watcher,于 Watcher constructor 内赋值,vm._watchers 包括了watch 选项生成的 Watcher,但每个组件都只有一个 renderWatcher),同时调用Watcher.get(),进行第一次的依赖收集,将Dep.target 设置成自己,同时调用对应的getter (因为 Watcher 可能是通过 watch 选项生成的,先只讨论涉及渲染的 Watcher),将让对应的 data 中各个 Object (包括data自身) 对应的 Observer 对应的 dep 以及各个键值的 dep 们,将现在组件的 Watcher 添加到dep.subs 中,实现“订阅”

class Watcher {

  // ...
  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()
      // 实际上只是把新收集到的dep从newDeps中拷贝至deps
      this.cleanupDeps()
    }
    return value
  }

  // ...

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          // cb 即与渲染相关的函数,使用新旧值
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
  // ...
}

dep 的另一个作用是发布 dep.notify()

一个简单的实例:

<template>
  <div>{{ num }}</div>
  <button @click='increaseNum'> Click to add </button>
</template>

<script>
export default {
  data() {
    return {
      num: 1
    }
  },
  methods: {
    increaseNum() {
      this.num++
    }
  }
}
</script>

当点击按钮触发 click 事件,vue通过编译模板注册的指令 on 将把 dom 事件一并传给increaseNum 函数并调用,此时 num 将被设置一个新值 2 ,这时 setter 中调用 num 键对应的 dep.notify 通知在 get 阶段收集到的Watcher 准备更新

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)
          }
        }
      }
      return value
    },
    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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

做总结,订阅的流程为:

WatcherDep
Watcher.get()
pushTarget() to Dep.target
dep.depend() 调用 Watcher.addDep()getter 中 dep.depend() 将 Dep.target 订阅
Watcher.cleanupDeps
等待setter调用dep.notify()

发布的流程为:

附录:dep, observer 之于 data 选项如下所示:

export default {
  data() {
    return {
      // __ob__: Observer with a dep
      name: 'Foo', // dep
      address: {
        // __ob__: Observer with a dep
        province: 'Beijing', // dep
        city: 'Beijing' // dep
      }
    }
  }
}
WatcherDep
dep.notify()
watcher.update() 判断是否同步渲染,否则等待
scheduler 在合适的时机执行 watcher.run()
watcher.get() 获取新数据dep.depend() 重新收集依赖,过一遍订阅流程
watcher.cb.call(vm, val, oldVal) 开启新数据渲染流程dep 等待 setter 调用 dep.notify()