Vue2源码学习笔记 - 16.响应式原理—更新调度

前面几节文章我们已经学习了响应式的整个过程,其中在派发通知环节,不管是什么类型的 Watcher,都会执行到 Watcher.update 方法,然后由它了决策接下来的处理,我们继续来学习它之后的流程。

加入调度队列

update 方法中,计算属性设置 watcher.dirty 为真即结束流程,而渲染函数和常规的侦听属性则执行调用 queueWatcher(watcher) 的流程。

file: /src/core/observer/watcher.js

// Watcher class
update () {
  if (this.lazy) {
    // 计算属性执行此流程
    this.dirty = true
  } else if (this.sync) {
    // 当 sync 为真是执行此流程,同步方式的侦听属性
    this.run()
  } else {
    // 渲染Watcher 和常规侦听属性执行此流程
    queueWatcher(this)
  }
}

我们来看看 queueWatcher 的实现,它的实现单独划分了一个文件模块,上代码:

file: /src/core/observer/scheduler.js

...
export const MAX_UPDATE_COUNT = 100
const queue: Array<Watcher> = []
const activatedChildren: Array<Component> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0

// Reset the scheduler's state.
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  waiting = flushing = false
}

export let currentFlushTimestamp = 0

let getNow: () => number = Date.now

if (inBrowser && !isIE) {
  const performance = window.performance
  if (
    performance &&
    typeof performance.now === 'function' &&
    getNow() > document.createEvent('Event').timeStamp
  ) {
    getNow = () => performance.now()
  }
}

// Flush both queues and run the watchers.
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

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

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    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) {
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

export function queueActivatedComponent (vm: Component) {
  vm._inactive = false
  activatedChildren.push(vm)
}

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

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

queueWatcher 函数主要把 Watcher 对象放入 queue 数组中,并把 Watcher.id 存入 has,然后把函数 flushSchedulerQueue 作为参数调用一次 nextTick,通过 waiting 来限制在一个任务调度中只被调用一次。那么 flushSchedulerQueue 呢?它会在下一个任务调度中被执行,这个上一节学习 nextTick 时学习过。

Watcher 对象在放入 queue 时先判断其 id 不在 has 中,然后判断 flushing 其值为假则尚未调用 flushSchedulerQueue 刷新队列,这时直接 push 进 queue。如果 flushing 为真,则 flushSchedulerQueue 此刻正在被调用,那么把它插入到队列中剩余尚未刷新的,并且位于 id 比它大的对象前面(如果存在的话),id 比它小的对象后面(如果存在的话)。

vue 渲染flushSchedulerQueue 刷新队列

刷新调度队列

flushSchedulerQueue 根据执行环境设置为优先在微任务中执行,如果不支持则在宏任务中执行。

// Flush both queues and run the watchers.
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true // 正在刷新
  let watcher, id

  // 按 Watcher.id 从小到大 排序
  queue.sort((a, b) => a.id - b.id)

  // 遍历 queue 数组
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 执行 watcher.before 方法
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 执行 watcher.run 方法
    watcher.run()
    ...
  }

  // keep copies of post queues before resetting state
  // 激活的组件数组队列
  const activatedQueue = activatedChildren.slice()
  // 处理后的queue 复制到 updatedQueue数组队列
  const updatedQueue = queue.slice()

  // 重置调度状态
  resetSchedulerState()

  // 调用相关钩子函数
  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

在这个 flushSchedulerQueue 函数中,先对 queue 队列按 Watcher.id 从小到大排序,这是为了确保三件事:
1、组件从父到子被更新,因为父组件先于子组件被创建。
2、用户级的 Watcher.run(计算属性和侦听属性)先于 renderWatcher.run 被执行,因为它们都先于 renderWatcher 被创建。
3、当在父组件 Watcher.run 执行期间子组件被销毁,则该子组件的 watcher 皆被忽略。

在排序好之后就开始遍历执行 Watcher.run 方法,如果有 Watcher.before 则在此之前先执行它。在这个 Watcher.run 方法中,不同类型的 Watcher 执行不同操作:renderWatcher 在 run 中调用 updateComponent 重新渲染页面;watchWatcher 在 run 方法中执行用户定义的回调函数。

遍历执行完之后,调用 resetSchedulerState 恢复一些关键变量为初始状态,迎接下一次调度任务。然后调用相关的组件的 updated 和 activated 钩子函数。

总结:

在任务调度中,本质还是调用 nextTick 设置下一个调度任务,并在此回调函数中对由 Watcher 对象组成的队列排序并调用其接口方法 run 执行必要的操作,比如重新渲染页面和调用侦听属性的回调函数等,总体来说流程比较简单。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值