网络日志

vue3源码】六、scheduler

【vue3源码】六、scheduler

在前文分析watch的过程中,我们知道在effect的调度器中会将job推入不同的任务队列,以在不同时机执行job函数。本文将深入分析job的执行时机。

watch中,会根据flushscheduler进行不同处理。如果flushsync,代表同步,那么scheduler就是job,在依赖触发时,会直接执行job;如果schedulerpost,在依赖触发时,会调用一个queuePostRenderEffect函数;而默认情况,在依赖触发时,会根据当前有无组件实例,进行不同操作。

if (flush === 'sync') {
    scheduler = job as any
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        job()
      }
    }
  }

queuePostRenderEffect

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

如果开启了__FEATURE_SUSPENSE__了,queuePostRenderEffectqueueEffectWithSuspense,否则是queuePostFlushCb。这里为了方便理解,我们只看queuePostFlushCb

queuePostFlushCb

export function queuePostFlushCb(cb: SchedulerJobs) {
  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

queuePostFlushCb接收一个SchedulerJobs类型cb参数,对于SchedulerJobs的定义:

export interface SchedulerJob extends Function {
  // 一个标识
  id?: number
  // 是否为激活状态
  active?: boolean
  computed?: boolean
  allowRecurse?: boolean
  ownerInstance?: ComponentInternalInstance
}

export type SchedulerJobs = SchedulerJob | SchedulerJob[]

queuePostFlushCb内部调用了一个queueCb方法,并传入了四个变量,其中第一个是queuePostFlushCb的参数,而后三个是全局变量

const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0

接着看queueCb

queueCb

function queueCb(
  cb: SchedulerJobs,
  activeQueue: SchedulerJob[] | null,
  pendingQueue: SchedulerJob[],
  index: number
) {
  if (!isArray(cb)) {
    if (
      !activeQueue ||
      !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
    ) {
      pendingQueue.push(cb)
    }
  } else {
    pendingQueue.push(...cb)
  }
  queueFlush()
}

queueCb函数接收四个参数:cbactiveQueue(激活的队列)、pendingQueue(等待中的队列)、index(一个索引),

如果cb是数组,会将cb解构放入pendingQueue中;否则判断是否传入activeQueueactiveQueue中是否已经存在cb(如果cb.allowRecursetrue,从index+1处开始寻找,否则从index处开始寻找)。最后执行一个queueFlush函数。

queueFlush

// 当前是否有正在执行的任务
let isFlushing = false
// 当前是否有正在等待执行的任务
let isFlushPending = false

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    // 正在执行的promise
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

queueFlush中的逻辑比较简单。如果没有正在执行的任务并且也没有正在等待中的任务,则将isFlushPending置为true,同时将flushJobs放入一个微任务队列。

flushJobs

function flushJobs(seen?: CountMap) {
  // 将isFlushPending置为false,因为已经进入执行任务中的状态
  isFlushPending = false
  // 正在执行任务
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }

  // 执行pendingPreFlushCbs中的任务
  flushPreFlushCbs(seen)

  // 将queue按job.id升序
  queue.sort((a, b) => getId(a) - getId(b))

  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 执行queue中的任务
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        if (__DEV__ && check(job)) {
          continue
        }
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    // 重置flushIndex
    flushIndex = 0
    // 清空queue
    queue.length = 0

    // 执行pendingPostFlushCbs中的任务
    flushPostFlushCbs(seen)

    // 当前没有正在执行的任务
    isFlushing = false
    currentFlushPromise = null
    // 执行剩余任务
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

flushJobs中会依次执行pendingPreFlushCbsqueuependingPostFlushCbs中的任务。

再来看queuePreFlushCb

queuePreFlushCb

const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

queuePreFlushCb方法会将cb放入一个pendingPreFlushCbs数组。

通过前文我们知道最终pendingPreFlushCbsqueuependingPostFlushCbs会按顺序依次执行,pendingPreFlushCbs中保存的是flush===pre时的jobpendingPostFlushCbs中保存的是flush===post时的job。那么queue中是什么呢?queue中保存的组件的更新函数。

const effect = (instance.effect = new ReactiveEffect(
  componentUpdateFn,
  () => queueJob(instance.update),
  instance.scope
))

queueJob

export function queueJob(job: SchedulerJob) {
  // queue.length为0或queue中不存在job,并且job不等于currentPreFlushParentJob
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    if (job.id == null) {
      queue.push(job)
    } else {
      // 替换重复id的job
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}

nextTick

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  // 将fn加入到一个微任务队列,它会在p执行完之后执行
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

在每次向pendingPreFlushCbsqueuependingPostFlushCbs中放入任务时,都会执行queueFlush()方法,queueFlush方法会更新currentFlushPromise为最新的promise。所以使用nextTick传入的函数会在flushJobs之后执行。