【vue3源碼】六、scheduler

【vue3源碼】六灰蛙、scheduler

scheduler即調(diào)度器是vue3中比較重要的一個(gè)概念胰耗。通過(guò)scheduler進(jìn)行調(diào)度任務(wù)(job)限次,保證了vue中相關(guān)API及生命周期函數(shù)、組件渲染順序的正確性。

我們知道卖漫,使用watchEffect進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí)费尽,偵聽(tīng)器將會(huì)在組件渲染之前執(zhí)行;watchSyncEffect進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí)羊始,偵聽(tīng)器在依賴(lài)發(fā)生變化后立即執(zhí)行旱幼;而watchPostEffect進(jìn)行偵聽(tīng)數(shù)據(jù)源時(shí),偵聽(tīng)器會(huì)在組件渲染后才執(zhí)行突委。針對(duì)不同的偵聽(tīng)器的執(zhí)行順序柏卤,就是通過(guò)scheduler進(jìn)行統(tǒng)一調(diào)度而實(shí)現(xiàn)的。

scheduler的實(shí)現(xiàn)

scheduler中主要通過(guò)三個(gè)隊(duì)列實(shí)現(xiàn)任務(wù)調(diào)度匀油,這三個(gè)對(duì)列分別為:

  • pendingPreFlushCbs:組件更新前置任務(wù)隊(duì)列
  • queue:組件更新任務(wù)隊(duì)列
  • pendingPostFlushCbs:組件更新后置任務(wù)隊(duì)列

如何使用這幾個(gè)隊(duì)列缘缚?vue中有三個(gè)方法分別用來(lái)進(jìn)行對(duì)pendingPreFlushCbsqueue敌蚜、pendingPostFlushCbs入隊(duì)操作桥滨。

// 前置任務(wù)隊(duì)列入隊(duì)
export function queuePreFlushCb(cb: SchedulerJob) {
  queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
}

// 后置任務(wù)隊(duì)列入隊(duì)
export function queuePostFlushCb(cb: SchedulerJobs) {
  queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
}

function queueCb(
  cb: SchedulerJobs,
  activeQueue: SchedulerJob[] | null,
  pendingQueue: SchedulerJob[],
  index: number
) {
  // 如果cb不是數(shù)組
  if (!isArray(cb)) {
    // 激活隊(duì)列為空或cb不在激活隊(duì)列中,需要將cb添加到對(duì)應(yīng)隊(duì)列中
    if (
      !activeQueue ||
      !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
    ) {
      pendingQueue.push(cb)
    }
  }
  // cb是數(shù)組
  else {
    // 如果 cb 是一個(gè)數(shù)組弛车,那么它是一個(gè)組件生命周期鉤子
    // 其已經(jīng)被去重了齐媒,因此我們可以在此處跳過(guò)重復(fù)檢查以提高性能
    pendingQueue.push(...cb)
  }
  queueFlush()
}

// queue隊(duì)列入隊(duì)
export function queueJob(job: SchedulerJob) {
  // 當(dāng)滿(mǎn)足以下情況中的一種才可以入隊(duì)
  // 1. queue長(zhǎng)度為0
  // 2. queue中不存在job(如果job是watch()回調(diào),搜索從flushIndex + 1開(kāi)始纷跛,否則從flushIndex開(kāi)始)喻括,并且job不等于currentPreFlushParentJob
  if (
    (!queue.length ||
      !queue.includes(
        job,
        isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
      )) &&
    job !== currentPreFlushParentJob
  ) {
    // job.id為null直接入隊(duì)
    if (job.id == null) {
      queue.push(job)
    } else {
      // 插隊(duì),插隊(duì)后queue索引區(qū)間[flushIndex + 1, end]內(nèi)的job.id是非遞減的
      // findInsertionIndex方法通過(guò)二分法尋找[flushIndex + 1, end]區(qū)間內(nèi)大于等于job.id的第一個(gè)索引
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}

三個(gè)隊(duì)列的入隊(duì)幾乎是相似的贫奠,都不允許隊(duì)列中存在重復(fù)job(從隊(duì)列的flushIndexflushIndex + 1開(kāi)始搜索)双妨。不同的是queue允許插隊(duì)。

queueFlush

job入隊(duì)后叮阅,會(huì)調(diào)用一個(gè)queueFlush函數(shù):

function queueFlush() {
  // isFlushing表示是否正在執(zhí)行隊(duì)列
  // isFlushPending表示是否正在等待執(zhí)行隊(duì)列
  // 如果此時(shí)未在執(zhí)行隊(duì)列也沒(méi)有正在等待執(zhí)行隊(duì)列刁品,則需要將isFlushPending設(shè)置為true,表示隊(duì)列進(jìn)入等待執(zhí)行狀態(tài)
  // 同時(shí)在下一個(gè)微任務(wù)隊(duì)列執(zhí)行flushJobs浩姥,即在下一個(gè)微任務(wù)隊(duì)列執(zhí)行隊(duì)列
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

為什么需要將flushJobs放入下一個(gè)微任務(wù)隊(duì)列挑随,而不是宏任務(wù)隊(duì)列?

首先微任務(wù)比宏任務(wù)有更高的優(yōu)先級(jí)勒叠,當(dāng)同時(shí)存在宏任務(wù)和微任務(wù)時(shí)兜挨,會(huì)先執(zhí)行全部的微任務(wù),然后再執(zhí)行宏任務(wù)眯分,這說(shuō)明通過(guò)微任務(wù)拌汇,可以將flushJobs盡可能的提前執(zhí)行。如果使用宏任務(wù)弊决,如果在queueJob之前有多個(gè)宏任務(wù)噪舀,則必須等待這些宏任務(wù)執(zhí)行完后魁淳,才能執(zhí)行queueJob,這樣以來(lái)flushJobs的執(zhí)行就會(huì)非秤氤靠后界逛。

flushJobs

flushJobs中會(huì)依次執(zhí)行pendingPreFlushCbsqueue纺座、pendingPostFlushCbs中的任務(wù)息拜,如果此時(shí)還有剩余job,則繼續(xù)執(zhí)行flushJobs净响,知道將三個(gè)隊(duì)列中的任務(wù)都執(zhí)行完少欺。

function flushJobs(seen?: CountMap) {
  // 將isFlushPending置為false,isFlushing置為true
  // 因?yàn)榇藭r(shí)已經(jīng)要開(kāi)始執(zhí)行隊(duì)列了
  isFlushPending = false
  isFlushing = true
  if (__DEV__) {
    seen = seen || new Map()
  }

  // 執(zhí)行前置任務(wù)隊(duì)列
  flushPreFlushCbs(seen)

  // queue按job.id升序排列
  // 這可確保:
  // 1. 組件從父組件先更新然后子組件更新馋贤。(因?yàn)?parent 總是在 child 之前創(chuàng)建赞别,所以它的redner effect會(huì)具有較高的優(yōu)先級(jí)) 
  // 2. 如果在 parent 組件更新期間卸載組件,則可以跳過(guò)其更新
  queue.sort((a, b) => getId(a) - getId(b))

  // 用于檢測(cè)是否是無(wú)限遞歸掸掸,最多 100 層遞歸氯庆,否則就報(bào)錯(cuò)蹭秋,只會(huì)開(kāi)發(fā)模式下檢查
  const check = __DEV__
    ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
    : NOOP

  try {
    // 執(zhí)行queue中的任務(wù)
    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 {
    // 清空queue并將flushIndex重置為0
    flushIndex = 0
    queue.length = 0
    
    // 執(zhí)行后置任務(wù)隊(duì)列
    flushPostFlushCbs(seen)
    
    // 將isFlushing置為false扰付,說(shuō)明此時(shí)任務(wù)已經(jīng)執(zhí)行完
    isFlushing = false
    currentFlushPromise = null
    // 執(zhí)行剩余job
    // post隊(duì)列執(zhí)行過(guò)程中可能有job加入,繼續(xù)調(diào)用flushJobs執(zhí)行剩余job
    if (
      queue.length ||
      pendingPreFlushCbs.length ||
      pendingPostFlushCbs.length
    ) {
      flushJobs(seen)
    }
  }
}

flushPreFlushCbs

flushPreFlushCbs用來(lái)執(zhí)行pendingPreFlushCbs中的job仁讨。

export function flushPreFlushCbs(
  seen?: CountMap,
  parentJob: SchedulerJob | null = null
) {
  // 有job才執(zhí)行
  if (pendingPreFlushCbs.length) {
    // 賦值父job
    currentPreFlushParentJob = parentJob
    // 去重并將隊(duì)列賦值給activePreFlushCbs
    activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
    // 清空pendingPreFlushCbs
    pendingPreFlushCbs.length = 0
    if (__DEV__) {
      seen = seen || new Map()
    }
    // 循環(huán)執(zhí)行job
    for (
      preFlushIndex = 0;
      preFlushIndex < activePreFlushCbs.length;
      preFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
      ) {
        continue
      }
      activePreFlushCbs[preFlushIndex]()
    }
    // 執(zhí)行完畢后將activePreFlushCbs重置為null羽莺、preFlushIndex重置為0、currentPreFlushParentJob重置為null
    activePreFlushCbs = null
    preFlushIndex = 0
    currentPreFlushParentJob = null
    // 遞歸flushPreFlushCbs洞豁,直到pendingPreFlushCbs為空停止
    flushPreFlushCbs(seen, parentJob)
  }
}

flushPostFlushCbs

export function flushPostFlushCbs(seen?: CountMap) {
  // flush any pre cbs queued during the flush (e.g. pre watchers)
  flushPreFlushCbs()
  // 存在job才執(zhí)行
  if (pendingPostFlushCbs.length) {
    // 去重
    const deduped = [...new Set(pendingPostFlushCbs)]
    // 清空pendingPostFlushCbs
    pendingPostFlushCbs.length = 0

    // #1947 already has active queue, nested flushPostFlushCbs call
    // 已經(jīng)存在activePostFlushCbs盐固,嵌套flushPostFlushCbs調(diào)用,直接return
    if (activePostFlushCbs) {
      activePostFlushCbs.push(...deduped)
      return
    }

    activePostFlushCbs = deduped
    if (__DEV__) {
      seen = seen || new Map()
    }
    
    // 按job.id升序
    activePostFlushCbs.sort((a, b) => getId(a) - getId(b))

    // 循環(huán)執(zhí)行job
    for (
      postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      if (
        __DEV__ &&
        checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
      ) {
        continue
      }
      activePostFlushCbs[postFlushIndex]()
    }
    // 重置activePostFlushCbs及丈挟、postFlushIndex
    activePostFlushCbs = null
    postFlushIndex = 0
  }
}

nextTick

export function nextTick<T = void>(
  this: T,
  fn?: (this: T) => void
): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

nextTick會(huì)在flushJobs執(zhí)行完成后才會(huì)執(zhí)行刁卜,組件的更新及onUpdatedonMounted等某些生命周期鉤子會(huì)在nextTick之前執(zhí)行曙咽。所以在nextTick.then中可以獲取到最新的DOM蛔趴。

哪些操作會(huì)交給調(diào)度器進(jìn)行調(diào)度?

  1. watchEffect例朱、watchPostEffect孝情,分別會(huì)將偵聽(tīng)器的執(zhí)行加入到前置任務(wù)隊(duì)列與后置任務(wù)隊(duì)列。
function doWatch() {
  // ...
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
            hasChanged(v, (oldValue as any[])[i])
          )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onCleanup
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      effect.run()
    }
  }
  
  if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    scheduler = () => queuePreFlushCb(job)
  }

  const effect = new ReactiveEffect(getter, scheduler)
  
  // ...
}
  1. 組件的更新函數(shù):
const setupRenderEffect = () => {
  // ...

  const componentUpdateFn = () => {
    //... 
  }

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

  const update: SchedulerJob = (instance.update = () => effect.run())
  update.id = instance.uid
  
  // ...
}
  1. onMounted洒嗤、onUpdated箫荡、onUnmountedTransitionenter鉤子等一些鉤子函數(shù)會(huì)被放到后置任務(wù)隊(duì)列
export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

const mountElement = () => {
  // ...

  if (
    (vnodeHook = props && props.onVnodeMounted) ||
    needCallTransitionHooks ||
    dirs
  ) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
      needCallTransitionHooks && transition!.enter(el)
      dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
    }, parentSuspense)
  }
}

const patchElement = () => {
  // ...

  if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
    queuePostRenderEffect(() => {
      vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
      dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
    }, parentSuspense)
  }
}

總結(jié)

scheduler通過(guò)三個(gè)隊(duì)列實(shí)現(xiàn)渔隶,在vue只需通過(guò)調(diào)用queuePreFlushCb羔挡、queuePostFlushCbqueueJob方法將job添加到對(duì)應(yīng)隊(duì)列中,不需要手動(dòng)控制job的執(zhí)行時(shí)機(jī)婉弹,完全將job的執(zhí)行時(shí)機(jī)交給了scheduler進(jìn)行調(diào)度睬魂。

三個(gè)隊(duì)列的特點(diǎn):

pendingPreFlushCbs queue pendingPostFlushCbs
執(zhí)行時(shí)機(jī) DOM更新前 queue中的job就包含組件的更新 DOM更新后
是否允許插隊(duì) 不允許 允許 不允許
job執(zhí)行順序 按入隊(duì)順序執(zhí)行,先進(jìn)先出 job.id升序順序執(zhí)行job镀赌。保證父子組件的更新順序 job.id升序順序執(zhí)行job

scheduler中通過(guò)Promise.resolve()將隊(duì)列中job的執(zhí)行(即flushJobs)放入到下一個(gè)微任務(wù)隊(duì)列中氯哮,而nextTick.then中回調(diào)的執(zhí)行又會(huì)被放到下一個(gè)微任務(wù)隊(duì)列。等到nextTick.then中回調(diào)的執(zhí)行商佛,隊(duì)列中的job已經(jīng)執(zhí)行完畢喉钢,此時(shí)DOM已經(jīng)更新完畢,所以在nextTick.then中就可以獲取到更新后的DOM良姆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末肠虽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子玛追,更是在濱河造成了極大的恐慌税课,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痊剖,死亡現(xiàn)場(chǎng)離奇詭異韩玩,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)陆馁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)找颓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人叮贩,你說(shuō)我怎么就攤上這事击狮。” “怎么了益老?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵彪蓬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我捺萌,道長(zhǎng)档冬,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任互婿,我火速辦了婚禮捣郊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘慈参。我一直安慰自己呛牲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布驮配。 她就那樣靜靜地躺著娘扩,像睡著了一般着茸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琐旁,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天涮阔,我揣著相機(jī)與錄音,去河邊找鬼灰殴。 笑死敬特,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的牺陶。 我是一名探鬼主播伟阔,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼掰伸!你這毒婦竟也來(lái)了皱炉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤狮鸭,失蹤者是張志新(化名)和其女友劉穎合搅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體歧蕉,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡灾部,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了廊谓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梳猪。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡麻削,死狀恐怖蒸痹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情呛哟,我是刑警寧澤叠荠,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站扫责,受9級(jí)特大地震影響榛鼎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鳖孤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一者娱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧苏揣,春花似錦黄鳍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)藏古。三九已至,卻和暖如春忍燥,著一層夾襖步出監(jiān)牢的瞬間拧晕,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工梅垄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留厂捞,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓队丝,卻偏偏與公主長(zhǎng)得像蔫敲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子炭玫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容