# scheduler

<template>
  <div id="test">{{state}}<div>
  <button @click="setState">set</botton>
</template>
<script>
export default {
  setup () {
    const state = ref('Hello, Vue')
    const setState = () => {
      state.value = 'Hello, scheduler'
      console.log(document.getElementById('test').innerHTML)
    }
    return {
      state,
      setState
    }
  }
}
</script>

回顾Vue中一个经典的例子,改变模板中的响应式数据后立刻获取DOM节点的内容,结果是获取到的内容还是旧的,这说明在Vue中组件更新是一个异步的过程。这也不难理解,如果每次修改响应式数据 Vue都同步执行一次组件更新的话,会带来很大的性能问题,所以Vue会收集一个Tick内触发的所有更新,一次性执行,之前我们分析了patch过程,也知道了组件的更新会触发组件Effect的重新执行:

instance.update = effect(function componentEffect() {
  ...
}, {
  scheduler: queueJob
})

可以看到在创建组件effect时,option传入了scheduler,根据之前分析effect时提到的,当option传了scheduler方法时,触发effect重新时会执行scheduler,而不是effect本身,也就是组件更新时其实执行的是queueJob方法:

// 任务队列
const queue: (SchedulerJob | null)[] = []
// 当前执行到任务队列的位置
let flushIndex = 0
export function queueJob(job: SchedulerJob) {
  // the dedupe search uses the startIndex argument of Array.includes()
  // by default the search index includes the current job that is being run
  // so it cannot recursively trigger itself again.
  // if the job is a watch() callback, the search will start with a +1 index to
  // allow it recursively trigger itself - it is the user's responsibility to
  // ensure it doesn't end up in an infinite loop.
  // 如果队列是空,并且传入的job不在队列中
  // 查找队列中是否存在传入的job时,根据job是否可以递归执行本身来决定判断的位置
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    // 将job添加到队列中,执行queueFlush
    queue.push(job)
    queueFlush()
  }
}
// 第一次执行queueFlush时将isFlushPending设置为true,并通过Promise.resolve().then
// 将执行任务队列的flushJobs放到微任务中执行
// 所以在一个tick内的宏任务中添加的job都会先push到队列中
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}
function flushJobs(seen?: CountMap) {
  // 更改flushing状态
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }
  // 执行一些需要在任务队列之前执行的钩子
  // 例如pre watch等等
  flushPreFlushCbs(seen)

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  // Jobs can never be null before flush starts, since they are only invalidated
  // during execution of another flushed job.
  // 1、按effect id从小到大排列队列中的render effect,因为组件更新是从父到子的,父组件的effect id < 子组件的effect id
  // 2、当一个组件已经销毁时,会调用stop方法将停止该effect,跳过它的更新
  // 队列中的effect有可能在队列开始执行之后变为null,因为存在父组件更新后触发子组件的更新,而子组件的render effect刚好已经在队列中这种情况
  // 那么在子组件执行updateComponent时会检查当前组件effect是否已经在更新队列中,如果存在则将队列中的effect设置为null
  queue.sort((a, b) => getId(a!) - getId(b!))

  try {
    // 遍历队列执行队列中的render effect
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job) {
        if (__DEV__) {
          checkRecursiveUpdates(seen!, job)
        }
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0
    // 执行一些需要在任务队列之后执行的钩子
    // 例如updated
    flushPostFlushCbs(seen)

    isFlushing = false
    currentFlushPromise = null
    // some postFlushCb queued jobs!
    // keep flushing until it drains.
    // 在执行队列的过程中,如果添加了其他的队列,或者钩子
    // 继续调用flushJobs直到队列为空
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}