Vue源码学习3.3:派发更新

396 阅读7分钟

通过上一节分析我们了解了响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节来详细分析这个过程。

我们看下 defineReactivesetter 的逻辑:

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

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    getfunction reactiveGetter ({
      // ...
    },
    setfunction reactiveSetter (newVal{
      const value = getter ? getter.call(obj) : val
      // 新旧值相同,或都为 NaN 的情况
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      
      // ...
      
      // #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()
    }
  })
}

setter 的逻辑有 2 个关键的点

  • childOb = !shallow && observe(newVal):对新设置的值变成一个响应式对象;
  • dep.notify():通知所有的订阅者

1. 触发setter

当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法, 它是 Dep 的一个实例方法,定义在 src/core/observer/dep.js 中:

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()
    }
  }
}
  • 首先浅拷贝 subs 返回一个新的数组
  • 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法
class Watcher {
  // ...
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

在这里我们只关心 update 函数其实就是调用了 queueWatcher(this)

2. queueWatcher

下面来看下 queueWatcher 的实现:

// src/core/observer/scheduler.js
const queue: Array<Watcher> = [] // watcher 队列
let has: { [key: number]: ?true } = {} // watcher id map
let waiting = false // 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
let flushing = false // 是否正在调用 flushSchedulerQueue
let index = 0 // 当前正在作业的 watcher 在 queue 数组的下标

export function queueWatcher (watcher: Watcher{
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher) // push watcher
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 10, watcher) // 插入watcher
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}
  • Vue 并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue
  • has 对象保证同一个 Watcher 只添加一次,但是当执行 flushSchedulerQueue 的过程中,watcher 是可以被添加进队列的,因为在 flushSchedulerQueue 遍历 queue 的时候会执行 has[id] = null
  • 接着对 flushing 的判断:
    • false 表示还没有调用 flushSchedulerQueue,此时将 watcher 推入 queue 队列。
    • else 部分的逻辑稍后再说。
  • waiting:保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次。
    • nextTick:在之后章节会介绍,目前就可以理解它是在下一个 tick,也就是异步的去执行 flushSchedulerQueue

Push a watcher into the watcher queue. Jobs with duplicate IDs will be skipped unless it's pushed when the queue is being flushed.

3. flushSchedulerQueue

接下来我们来看 flushSchedulerQueue 的实现,它的定义在 src/core/observer/scheduler.js 中。

// src/core/observer/scheduler.js
const queue: Array<Watcher> = [] // watcher 队列
let has: { [key: number]: ?true } = {} // watcher id map
let waiting = false // 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次
let flushing = false // 是否正在调用 flushSchedulerQueue
let index = 0 // 当前正在作业的 watcher 在 queue 数组的下标

let circular: { [key: number]: number } = {}
export const MAX_UPDATE_COUNT = 100

function flushSchedulerQueue ({
  flushing = true
  let watcher, id

  // 排序
  queue.sort((a, b) => a.id - b.id)

  // 每次遍历都需要重新计算 length
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 执行 beforeUpdate 钩子
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // 状态恢复前保留队列副本
  const updatedQueue = queue.slice()
  
  // 状态恢复
  resetSchedulerState()

  // 调用 updated 钩子
  callUpdatedHooks(updatedQueue)

  // ...
}

3.1 队列排序

queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:

  1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

  2. 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

  3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

3.2 队列遍历

在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()

这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher:修改数据值从而触发 setter,这样会再次执行到 queueWatcher,如下:

// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher{
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher) // push watcher
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 10, watcher) // 插入watcher
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

可以看到,这时候 flushingtrue,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcherid 比当前队列中 watcherid 大的位置。因此 queue 的长度发生了变化。

3.2.1 watcher.run()

下面来看看当执行 watcher.run() 时发生了什么:

class Watcher {
  // ...
  run () {
    // active: true
    if (this.active) {
      // 渲染watcher始终返回undefined
      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
      ) {
        const oldValue = this.value
        this.value = value
        
        // 简化后的
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

通过 this.get() 得到它当前的值,然后做判断,如果满足以下条件之一:

  • 新旧值不等
  • 新值是对象类型
  • deep 模式

则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。

那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 this.getter 方法,也就是 updateComponent

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

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同,在之后的章节会介绍。

3.2.2 循环判断

if (process.env.NODE_ENV !== 'production' && has[id] != null) {
  circular[id] = (circular[id] || 0) + 1
  if (circular[id] > MAX_UPDATE_COUNT) {
    warn(
      'You may have an infinite update loop ' + (
        watcher.user
          ? `in watcher with expression "${watcher.expression}"`
          : `in a component render function.`
      ),
      watcher.vm
    )
    break
  }
}

每次遍历 queue 时,用 circular[id] 来记录 watcher 对象的循环次数,当大于 MAX_UPDATE_COUNT 时认为是死循环

比如有以下例子:

<script>
export default {
  data() {
    return {
      msg: 1
    }
  },
  watch: {
    msg() {
      this.msg++
    }
  },
  mounted() {
    this.msg++
  }
}
</script>

会产生如下报错:

3.3 状态恢复

这个过程就是执行 resetSchedulerState 函数,它的定义在 src/core/observer/scheduler.js 中。

// src/core/observer/scheduler.js
const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

function resetSchedulerState ({
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

3.4 调用 updated 钩子

flushSchedulerQueue 函数的执行中还会执行 beforeUpdateupdated 钩子,这部分之前章节已经介绍过了,这里就不多说了。