Vue源码窥探之依赖收集

派发更新主要是在我们修改响应式数据的时候可以得到通知。
Vue源码窥探之依赖收集_第1张图片
当修改响应式对象地数据时,会触发 setter ,里面有两个关键点,一个是 childOb = !shallow && observe(newVal),如果 shallow 为 false 的情况下,会对新设置的值变成响应式对象;另一个是 dep.notify(),通知所有订阅者。

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

dep.notify 方法是遍历 subs 中的每一个 watcher 分别执行 update 方法。我们来看一下 watcher 中的实现。
Vue源码窥探之依赖收集_第2张图片
在我们当前的逻辑主线中,我们主要分析一般组件数据更新的场景,所以会走到最后一个逻辑 queueWatcher(this) 中,那我们就需要看一下 queueWatcher 中到底发生了什么?在 src/core/observer/cheduler.js 中,我们可以看到其定义:
Vue源码窥探之依赖收集_第3张图片
该方法主要负责推入一个 watcherwatcher queue 中,在队列中如果已经存在相同的 id 则该 watcher 将被跳过,除非它是在队列被刷新时被推送。
这里 Vue 出于性能上的考虑,在派发更新的时候并不会立即触发 watcher 的回调函数,而是把这些 watcher 先添加到一个队列中,然后在 nextTick 后执行 flushSchedulerQueue
需要注意的是,我们在调用 flushSchedulerQueue 之前,会先做一系列的拦截判断处理,首先用 has 对象来保证同一个 watcher 只添加一次;接着对 flushingwating 的判断,保证对 nextTick(flushSchedulerQueue) 的调用只有一次。
Vue源码窥探之依赖收集_第4张图片

  • 队列排序
    queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要确保以下几点:
    1,组件的更新是从上到下,从父到子的,所以父组件的创建过程是先于子组件,所以 watcher 的创建也是先父后子,那么我们回调函数的 watcher 的执行顺序也应该保持先父后子。
    2,用户的自定义 watcher 要优先于渲染 watcher 执行,因为用户自定义的 watcher 是在渲染 watcher 之前创建的。
    3,如果一个组件的父组件的 watcher 在执行期间被销毁,那么其对应的 watcher 执行都会被跳过,所以父组件的 watcher 应该先执行。

    其实,终究到底,就是确保先创建的 watcher 先执行。

  • 队列遍历
    在对 queue 排序后,接着就要遍历了,依次对 queue 中的每个 watcher 执行 watcher.run() 。需要特别注意的是,在遍历的时候需要每次对 queue.length 求值,因为在我们 watcher.run() 的时候用户可能再次添加新的 watcher,那久会执行到 queueWatcher ,如下:

    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // ...
      }
    }
    

    可以看到,这时候的 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入的 watcher 的 id 比当前队列中的 watcher 的 id 大的位置。把 wathcer 按照 id 的插入到队列中,所以 queue 的长度也发生了改变。

最后我们来看一下 watcher.run() 的逻辑。

class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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 {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
}

执行 watcher.run() ,先通过 this.get() 拿到当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二参数传入新值 value 和旧值 oldValue ,这就是我们自定义 watcher 的时候可以在回调函数中拿到新旧值的原因。这是 this.user = truewatcher,那么对于渲染 watcher 而言,在执行 this.get() 的时候,会执行 getter 方法:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

这就是为什么当我们修改组件相关的响应式数据的时候,会触发组件重新渲染的原因。接着就会执行 patch 过程,但是和组件的首次渲染略有不同。

你可能感兴趣的:(js,vue)