scheduler任务调度器

前置铺垫:schedler的源码尽管只有二百多行,并且与组件更新前后、更新中的所有执行的【工作】无关。【工作】在这里有比拟形象,了解起来比拟艰难。好在有一点就是咱们在上一篇的文章中有了解到一个工作:watch Effect。咱们就能够联合这个job,对scheduler进行剖析。在后续的文章中,当咱们解说到update阶段的时候,会回来再看下scheduler。到时候就能明确不少啦。

上面间接进入正题:

调度器在执行工作的过程中,次要将工作分为三个阶段,每个阶段两种状态:

  • 前置刷新阶段
  • 刷新阶段
  • 后置刷新阶段

每个阶段各有两种状态:

  • 正在期待刷新
  • 正在刷新

每次刷新的时候,通过Promise.resolve启动一个微工作,调用flushJob函数,先进行前置刷新工作,直至前置回调工作池为空,在刷新当前任务队列,当前任务队列刷新完结,最初刷新后置回调工作池,如此周而复始,直至三个工作池中的回调都刷新完结。

在解说watch的时候,咱们说过,watch effect会在组件update之前执行。这与用户定义的副作用函数 配置项fulsh无关。

  • flush: pre,默认值。watch Effectflush就是pre

    • 在创立watch的时候通过调用queuePreFlushCb(job),将副作用函数pushpendingPreFlushCbs
    • 当组件须要进行update的时候,会先遍历执行pendingPreFlushCbs池中的回调
    • 从而做到在组件update前进行刷新。
  • fulsh: post。可选 但不举荐

    • 当设置watch effectflushpost的时候就会调用queuePostFlushCb函数,将副作用函数pushpendingPostFlushCbs
    • 当queue中的工作执行完之后,就会遍历执行pendingPostFlushCbs中的工作
    • 从而做到在组件update后进行刷新

上面咱们一起看下这块相干的代码:

// 前置更新相干const pendingPreFlushCbs = []let activePreFlushCbs = nulllet preFlushIndex = 0// 后置更新相干const pendingPostFlushCbs  = []let activePostFlushCbs = nulllet postFlushIndex = 0function queuePreFlushCb(cb) {  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)}function queuePostFlushCb(cb) {  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)}function queueCb(cb, activeQueue, pendingQueue, index) {  if (!isArray(cb)) {    // cb不是数组    if (      !activeQueue ||      !activeQueue.includes(        cb,        (cb as SchedulerJob).allowRecurse ? index + 1 : index      )    ) {      // activeQueue不存在 || 从index+1地位开始activeQueue不蕴含cb      // watch job 会进来      pendingQueue.push(cb)    }  } else {    // 如果cb是一个数组,则它是一个组件生命周期挂钩,只能由一个作业触发,    // 该作业已在主队列中打消反复,sowe能够在此处跳过反复查看以进步性能    pendingQueue.push(...cb)  }  queueFlush()}

从下面的代码中能够复制往各阶段工作池中,push工作的次要是queueCb函数,queueCb函数次要负责对工作进行判断,当工作是数组时,会间接解构至待执行队列中,当工作非数组的时候,须要对工作进行判断,push的工作不能在正在执行的工作队列中存在,或者以后没有正在执行的工作队列。最初会调用queueFlush函数。

queueFlush函数会依据以后的状态进行判断,只有非正在刷新且非正在期待刷新的状态下。才会通过Promise.resolve启动微工作,刷新队列。

看下queueFlush的代码:

// 冲刷队列function queueFlush() {  // 如果没有正在刷新的 && 正在期待刷新的  // 则执行 flushJobs  if (!isFlushing && !isFlushPending) {    // 正在期待刷新    isFlushPending = true    // 启动微工作,开始刷新工作队列。    // flushJobs执行完结 将promise赋值给 currentFlushPromise    currentFlushPromise = resolvedPromise.then(flushJobs)  }}

当启动微工作刷新队列的时候,会将isFlushPending = true,示意开始期待刷新。当以后宏工作执行完结后,会执行相应的微工作队列,这时就会调用flushJobs函数。开始刷新队列。

以后宏工作有哪些,咱们先不关注。首先要晓得每个宏工作都会对应一个微工作队列,宏工作执行完结才会执行相应的微工作队列。

这也就是Vue所提到的【防止同一个“tick” 中多个状态扭转导致的不必要的反复调用,并异步刷新用户副作用函数】

function flushJobs(seen?: CountMap) {  //  期待刷新完结,开始刷新  isFlushPending = false  isFlushing = true  if (__DEV__) {    seen = seen || new Map()  }  //  前置刷新开始 jobs  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.  //  在刷新前对队列排序  // 1. 保障组件更新程序是从父组件到子组件(因为父组件总是在子组件之前创立,所以其渲染副作用的优先级将更小)  // 2.如果一个子组件在父组件更新期间卸载了,能够跳过该子组件的更新。  queue.sort((a, b) => getId(a) - getId(b))  try {    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {      const job = queue[flushIndex]      if (job && job.active !== false) {        if (__DEV__ && checkRecursiveUpdates(seen!, job)) {          continue        }        // 执行 job 函数        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)      }    }  } finally {    //  重置正在刷新队列    flushIndex = 0    queue.length = 0    //  刷新后置刷新jobs    flushPostFlushCbs(seen)    //  刷新完结    isFlushing = false          // 重置以后刷新的promise    // 最初再nextTick中会用到    currentFlushPromise = null    // some postFlushCb queued jobs!    // keep flushing until it drains.    //  如果还有当前任务或者,期待的估算新工作,或者期待的后刷新工作,则递归刷新    if (      queue.length ||      pendingPreFlushCbs.length ||      pendingPostFlushCbs.length    ) {      // 递归刷新      flushJobs(seen)    }  }}

flushJobs函数就是切入口,次要负责所有工作队列的刷新工作,前置工作的刷新次要是在该函数中调起flushPreFlushCbs(seen)函数,先去刷新前置工作池中的所有工作。

flushPreFlushCbs(seen) 函数代码:

export function flushPreFlushCbs(seen ,parentJob) {  if (pendingPreFlushCbs.length) {    currentPreFlushParentJob = parentJob    //  去重    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]    //   置预刷jobs array 为空    pendingPreFlushCbs.length = 0    if (__DEV__) {      seen = seen || new Map()    }    for (      preFlushIndex = 0;      preFlushIndex < activePreFlushCbs.length;      preFlushIndex++    ) {      if (        __DEV__ &&        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])      ) {        // 递归刷新查看        continue      }      //  执行job eg: watch job      //  watch 会在这里执行      activePreFlushCbs[preFlushIndex]()    }    //  重置    activePreFlushCbs = null    preFlushIndex = 0    currentPreFlushParentJob = null    // recursively flush until it drains    //  递归刷新预刷新jobs    flushPreFlushCbs(seen, parentJob)  }}

对于flushPreFlushCbs函数,咱们把次要关注点先放在:

  • 前置更新状态的切换,由pendingactive
  • 遍历执行前置工作池中的每个工作
  • 当遍历完结会重置以后状态及index
  • 递归调用flushPreFlushCbs,直至pendingPreFlushCbs工作池为空。
  • 次要是保障所有正在期待的队列会被执行到

有的同学可能会有疑难:既然曾经 通过 pendingPreFlushCbs.length = 0,将待执行工作池清空了,为什么还须要递归持续。

这个其实与遍历执行的工作无关,有的工作中,还会持续创立待执行工作,这时就会将创立的待执行工作持续push至待执行工作池。故须要递归遍历执行

flushPreFlushCbs函数执行完结后,就会进行以后遍历。即进入了flushing阶段,这时存在于queueupdate函数就会执行,组件就会进行更新。然而在执行queue中的工作的时候,须要对工作去重 排序,这些工作实现之后,才会遍历执行queue中的工作。

queue中的工作执行完结后,会通过 flushIndex = 0queue.length = 0,对以后队列进行重置。

随后就会调用flushPostFlushCbs函数,该函数会刷新后置刷新队列,同样的主逻辑:扭转后置刷新阶段状态,遍历执行后置刷新阶段工作池中的所有工作。

watch Effect flush: post的时候,这时就会遍历执行到watch effect

flushPostFlushCbs与后面两个函数不一样的是:没有进行递归刷新。次要目标是为了保障各阶段中工作能按:前置➡以后➡后置阶段的程序进行刷新!

flushPostFlushCbs函数的代码:

export function flushPostFlushCbs(seen?: CountMap) {  //  如果存在后置刷新工作  if (pendingPostFlushCbs.length) {    //  去重job    const deduped = [...new Set(pendingPostFlushCbs)]    //  正在期待的工作池 状况    pendingPostFlushCbs.length = 0    //  #1947 already has active queue, nested flushPostFlushCbs call    if (activePostFlushCbs) {      //  如果曾经有沉闷的队列,嵌套的flushPostFlushCbs调用      activePostFlushCbs.push(...deduped)      return    }    //  将期待的作为以后的工作    activePostFlushCbs = deduped    if (__DEV__) {      seen = seen || new Map()    }    //  对后置工作进行排序    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))    for (      postFlushIndex = 0;      postFlushIndex < activePostFlushCbs.length;      postFlushIndex++    ) {      if (        __DEV__ &&        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])      ) {        continue      }      //  执行后置工作      activePostFlushCbs[postFlushIndex]()    }    //  重置正在执行的工作池    activePostFlushCbs = null    postFlushIndex = 0  }}

flushPostFlushCbs函数执行完结的时候,就会回到flushJobs函数,通过 isFlushing = false重置刷新状态。

最初通过各个阶段工作池中时候有工作,再持续递归调用flushJobs函数。

如此往返,直至所有阶段的工作执行完结。

nextTick原理

咱们晓得nextTick API 会将回调提早到下次 DOM 更新循环之后执行。并会返回一个Promise

通过理解flushJobs函数,flushJobs函数次要就是通过Promsie.resolve执行的,当flushJobs函数执行完结,也就是Promsie.resolve更改状态的时候。

首先flushJobs函数会置空 currentFlushPromise。最初才会通过Promsie.resolve赋值给currentFlushPromise

当调用nextTick的时候,返回的promise,其实就是currentFlushPromise

能够再下来看下flushJobs函数中的代码。

nextTick代码:

const resolvedPromise: Promise<any> = Promise.resolve()let currentFlushPromise: Promise<void> | null = nullfunction nextTick(  this: ComponentPublicInstance | void,  fn?: () => void): Promise<void> {  const p = currentFlushPromise || resolvedPromise  return fn ? p.then(this ? fn.bind(this) : fn) : p}

其实明确了scheduler的调度过程,nextTick很好了解。

总结

通过剖析咱们晓得在Vue3scheduler任务调度器,在执行工作的过程中,次要分为三个阶段,前置刷新阶段、后置刷新阶段、以后刷新阶段(update阶段),每个阶段都有两种状态:期待刷新 & 正在刷新,每个阶段发生变化后,状态都会进行重置。并且是按 前置➡以后➡后置➡前置...的过程进行的,如此往返,直到各阶段工作池中的所有工作完结。nextTick是等所有阶段的刷新工作完结后返回的一个Promise.resolve

最初上一张图,总结下整个过程。

最初还是很(bu)真(yao)诚(lian)的举荐下我的公众号:coder狂想曲。您的关注就是对我创作的最大激励呐。