溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制

發(fā)布時間:2021-10-19 13:55:32 來源:億速云 閱讀:124 作者:iii 欄目:web開發(fā)

本篇內(nèi)容主要講解“如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制”吧!

點擊進(jìn)入React源碼調(diào)試倉庫。

在React的concurrent模式下,低優(yōu)先級任務(wù)執(zhí)行過程中,一旦有更高優(yōu)先級的任務(wù)進(jìn)來,那么這個低優(yōu)先級的任務(wù)會被取消,優(yōu)先執(zhí)行高優(yōu)先級任務(wù)。等高優(yōu)先級任務(wù)做完了,低優(yōu)先級任務(wù)會被重新做一遍。

我們用一個具體的例子來理解一下高優(yōu)先級任務(wù)插隊。

有這樣一個組件,state為0,進(jìn)入頁面,會調(diào)用setState將state加1,這個作為低優(yōu)先級任務(wù)。React開始進(jìn)行更新,在這個低優(yōu)先級任務(wù)尚未完成時,模擬按鈕點擊,state加2,這個作為高優(yōu)先級任務(wù)。可以看到,頁面上的數(shù)字變化為0 -> 2 -> 3,而不是0 -> 1 -> 3。這就說明,當(dāng)?shù)蛢?yōu)先級任務(wù)(加1)正在進(jìn)行時,高優(yōu)先級任務(wù)進(jìn)來了,而它會把state設(shè)置為2。由于高優(yōu)先級任務(wù)的插隊,設(shè)置state為1的低優(yōu)先級任務(wù)會被取消,先做高優(yōu)先級任務(wù),所以數(shù)字從0變成了2。而高優(yōu)先級任務(wù)完成之后,低優(yōu)先級任務(wù)會被重做,所以state再從2加到了3。

現(xiàn)象如下:

如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制

利用chrome的性能分析工具捕捉更新過程,可以明顯看到優(yōu)先級插隊的過程

如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制

完整的profile文件我保存下來了,可以載入到chrome中詳細(xì)查看:高優(yōu)先級插隊.json 。

可以再看一下這個過程中兩個任務(wù)優(yōu)先級在調(diào)度過程中的信息

如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制

點擊查看 高優(yōu)先級插隊示例代碼文件。

點擊查看 低優(yōu)先級任務(wù)饑餓問題示例代碼文件。

接下來我們就來從setState開始,探討一下這種插隊行為的本質(zhì),內(nèi)容涉及update對象的生成、發(fā)起調(diào)度、工作循環(huán)、高優(yōu)任務(wù)插隊、update對象的處理、低優(yōu)先級任務(wù)重做等內(nèi)容。

產(chǎn)生更新

當(dāng)調(diào)用setState時,意味著組件對應(yīng)的fiber節(jié)點產(chǎn)生了一個更新。setState實際上是生成一個update對象,調(diào)用enqueueSetState,將這個update對象連接到fiber節(jié)點的updateQueue鏈表中.

Component.prototype.setState = function(partialState, callback) {    this.updater.enqueueSetState(this, partialState, callback, 'setState');  };

enqueueSetState的職責(zé)是創(chuàng)建update對象,將它入隊fiber節(jié)點的update鏈表(updateQueue),然后發(fā)起調(diào)度。

enqueueSetState(inst, payload, callback) {      // 獲取當(dāng)前觸發(fā)更新的fiber節(jié)點。inst是組件實例      const fiber = getInstance(inst);      // eventTime是當(dāng)前觸發(fā)更新的時間戳      const eventTime = requestEventTime();      const suspenseConfig = requestCurrentSuspenseConfig();      // 獲取本次update的優(yōu)先級      const lane = requestUpdateLane(fiber, suspenseConfig);      // 創(chuàng)建update對象      const update = createUpdate(eventTime, lane, suspenseConfig);      // payload就是setState的參數(shù),回調(diào)函數(shù)或者是對象的形式。      // 處理更新時參與計算新狀態(tài)的過程      update.payload = payload;      // 將update放入fiber的updateQueue      enqueueUpdate(fiber, update);      // 開始進(jìn)行調(diào)度      scheduleUpdateOnFiber(fiber, lane, eventTime);    }

梳理一下enqueueSetState中具體做的事情:

找到fiber

首先獲取產(chǎn)生更新的組件所對應(yīng)的fiber節(jié)點,因為產(chǎn)生的update對象需要放到fiber節(jié)點的updateQueue上。然后獲取當(dāng)前這個update產(chǎn)生的時間,這與更新的饑餓問題相關(guān),我們暫且不考慮,而且下一步的suspenseConfig可以先忽略。

計算優(yōu)先級

之后比較重要的是計算當(dāng)前這個更新它的優(yōu)先級lane:

const lane = requestUpdateLane(fiber, suspenseConfig);

計算這個優(yōu)先級的時候,是如何決定根據(jù)什么東西去計算呢?這還得從React的合成事件說起。

事件觸發(fā)時,合成事件機(jī)制調(diào)用scheduler中的runWithPriority函數(shù),目的是以該交互事件對應(yīng)的事件優(yōu)先級去派發(fā)真正的事件流程。runWithPriority會將事件優(yōu)先級轉(zhuǎn)化為scheduler內(nèi)部的優(yōu)先級并記錄下來。當(dāng)調(diào)用requestUpdateLane計算lane的時候,會去獲取scheduler中的優(yōu)先級,以此作為lane計算的依據(jù)。

這部分的源碼在這里

創(chuàng)建update對象, 入隊updateQueue

根據(jù)lane和eventTime還有suspenseConfig,去創(chuàng)建一個update對象,結(jié)構(gòu)如下:

const update: Update<*> = {    eventTime,    lane,    suspenseConfig,    tag: UpdateState,    payload: null,    callback: null,    next: null,  };
  •  eventTime:更新的產(chǎn)生時間

  •  lane:表示優(yōu)先級

  •  suspenseConfig:任務(wù)掛起相關(guān)

  •  tag:表示更新是哪種類型(UpdateState,ReplaceState,F(xiàn)orceUpdate,CaptureUpdate)

  •  payload:更新所攜帶的狀態(tài)。

    •  在類組件中,有兩種可能,對象({}),和函數(shù)((prevState, nextProps):newState => {})

    •  根組件中,為React.element,即ReactDOM.render的第一個參數(shù)

  •  callback:可理解為setState的回調(diào)

  •  next:指向下一個update的指針

再之后就是去調(diào)用React任務(wù)執(zhí)行的入口函數(shù):scheduleUpdateOnFiber去調(diào)度執(zhí)行更新任務(wù)了。

現(xiàn)在我們知道了,產(chǎn)生更新的fiber節(jié)點上會有一個updateQueue,它包含了剛剛產(chǎn)生的update。下面該進(jìn)入scheduleUpdateOnFiber了,開始進(jìn)入真正的調(diào)度流程。通過調(diào)用scheduleUpdateOnFiber,render階段的構(gòu)建workInProgress樹的任務(wù)會被調(diào)度執(zhí)行,這個過程中,fiber上的updateQueue會被處理。

調(diào)度準(zhǔn)備

React的更新入口是scheduleUpdateOnFiber,它區(qū)分update的lane,將同步更新和異步更新分流,讓二者進(jìn)入各自的流程。但在此之前,它會做幾個比較重要的工作:

  •  檢查是否是無限更新,例如在render函數(shù)中調(diào)用了setState。

  •  從產(chǎn)生更新的節(jié)點開始,往上一直循環(huán)到root,目的是將fiber.lanes一直向上收集,收集到父級節(jié)點的childLanes中,childLanes是識別這個fiber子樹是否需要更新的關(guān)鍵。

  •  在root上標(biāo)記更新,也就是將update的lane放到root.pendingLanes中,每次渲染的優(yōu)先級基準(zhǔn):renderLanes就是取自root.pendingLanes中最緊急的那一部分lanes。

這三步可以視為更新執(zhí)行前的準(zhǔn)備工作。

第1個可以防止死循環(huán)卡死的情況。

第2個,如果fiber.lanes不為空,則說明該fiber節(jié)點有更新,而fiber.childLanes是判斷當(dāng)前子樹是否有更新的重要依據(jù),若有更新,則繼續(xù)向下構(gòu)建,否則直接復(fù)用已有的fiber樹,就不往下循環(huán)了,可以屏蔽掉那些無需更新的fiber節(jié)點。

第3個是將當(dāng)前update對象的lane加入到root.pendingLanes中,保證真正開始做更新任務(wù)的時候,獲取到update的lane,從而作為本次更新的渲染優(yōu)先級(renderLanes),去更新。

實際上在更新時候獲取到的renderLanes,并不一定包含update對象的lane,因為有可能它只是一個較低優(yōu)先級的更新,有可能在它前面有高優(yōu)先級的更新

梳理完scheduleUpdateOnFiber的大致邏輯之后,我們來看一下它的源碼:

export function scheduleUpdateOnFiber(    fiber: Fiber,    lane: Lane,    eventTime: number,  ) {    // 第一步,檢查是否有無限更新    checkForNestedUpdates();    ...    // 第二步,向上收集fiber.childLanes    const root = markUpdateLaneFromFiberToRoot(fiber, lane);   ...    // 第三步,在root上標(biāo)記更新,將update的lane放到root.pendingLanes    markRootUpdated(root, lane, eventTime);    ...    // 根據(jù)Scheduler的優(yōu)先級獲取到對應(yīng)的React優(yōu)先級    const priorityLevel = getCurrentPriorityLevel();    if (lane === SyncLane) {      // 本次更新是同步的,例如傳統(tǒng)的同步渲染模式      if (        (executionContext & LegacyUnbatchedContext) !== NoContext &&        (executionContext & (RenderContext | CommitContext)) === NoContext      ) {        // 如果是本次更新是同步的,并且當(dāng)前還未渲染,意味著主線程空閑,并沒有React的        // 更新任務(wù)在執(zhí)行,那么調(diào)用performSyncWorkOnRoot開始執(zhí)行同步任務(wù)        ...        performSyncWorkOnRoot(root);      } else {        // 如果是本次更新是同步的,不過當(dāng)前有React更新任務(wù)正在進(jìn)行,        // 而且因為無法打斷,所以調(diào)用ensureRootIsScheduled       // 目的是去復(fù)用已經(jīng)在更新的任務(wù),讓這個已有的任務(wù)        // 把這次更新順便做了        ensureRootIsScheduled(root, eventTime);       ...      }    } else {      ...      // Schedule other updates after in case the callback is sync.      // 如果是更新是異步的,調(diào)用ensureRootIsScheduled去進(jìn)入異步調(diào)度      ensureRootIsScheduled(root, eventTime);      schedulePendingInteractions(root, lane);    }    ...  }

  scheduleUpdateOnFiber 的完整源碼在這里,這里是第二步:markUpdateLaneFromFiberToRoot 和 第三步: markRootUpdated的完整源碼,我都做了注釋。

經(jīng)過了前面的準(zhǔn)備工作后,scheduleUpdateOnFiber最終會調(diào)用ensureRootIsScheduled,來讓React任務(wù)被調(diào)度,這是一個非常重要的函數(shù),它關(guān)乎同等或較低任務(wù)的收斂、

高優(yōu)先級任務(wù)插隊和任務(wù)饑餓問題,下面詳細(xì)講解它。

開始調(diào)度

在開始講解ensureRootIsScheduled之前,我們有必要弄清楚React的更新任務(wù)的本質(zhì)。

React任務(wù)的本質(zhì)

一個update的產(chǎn)生最終會使React在內(nèi)存中根據(jù)現(xiàn)有的fiber樹構(gòu)建一棵新的fiber樹,新的state的計算、diff操作、以及一些生命周期的調(diào)用,都會在這個構(gòu)建過程中進(jìn)行。這個整體的構(gòu)建工作被稱為render階段,這個render階段整體就是一個完整的React更新任務(wù),更新任務(wù)可以看作執(zhí)行一個函數(shù),這個函數(shù)在concurrent模式下就是performConcurrentWorkOnRoot,更新任務(wù)的調(diào)度可以看成是這個函數(shù)被scheduler按照任務(wù)優(yōu)先級安排它何時執(zhí)行。

Scheduler的調(diào)度和React的調(diào)度是兩個完全不同的概念,React的調(diào)度是協(xié)調(diào)任務(wù)進(jìn)入哪種Scheduler的調(diào)度模式,它的調(diào)度并不涉及任務(wù)的執(zhí)行,而Scheduler是調(diào)度機(jī)制的真正核心,它是實打?qū)嵉厝?zhí)行任務(wù),沒有它,React的任務(wù)再重要也無法執(zhí)行,希望讀者加以區(qū)分這兩種概念。

當(dāng)一個任務(wù)被調(diào)度之后,scheduler就會生成一個任務(wù)對象(task),它的結(jié)構(gòu)如下所示,除了callback之外暫時不需要關(guān)心其他字段的含義。

var newTask = {     id: taskIdCounter++,     // 任務(wù)函數(shù),也就是 performConcurrentWorkOnRoot     callback,     // 任務(wù)調(diào)度優(yōu)先級,由即將講到的任務(wù)優(yōu)先級轉(zhuǎn)化而來     priorityLevel,     // 任務(wù)開始執(zhí)行的時間點     startTime,     // 任務(wù)的過期時間     expirationTime,     // 在小頂堆任務(wù)隊列中排序的依據(jù)     sortIndex: -1,   };

每當(dāng)生成了一個這樣的任務(wù),它就會被掛載到root節(jié)點的callbackNode屬性上,以表示當(dāng)前已經(jīng)有任務(wù)被調(diào)度了,同時會將任務(wù)優(yōu)先級存儲到root的callbackPriority上,

表示如果有新的任務(wù)進(jìn)來,必須用它的任務(wù)優(yōu)先級和已有任務(wù)的優(yōu)先級(root.callbackPriority)比較,來決定是否有必要取消已經(jīng)有的任務(wù)。

所以在調(diào)度任務(wù)的時候,任務(wù)優(yōu)先級是不可或缺的一個重要角色。

任務(wù)優(yōu)先級

任務(wù)本身是由更新產(chǎn)生的,因此任務(wù)優(yōu)先級本質(zhì)上是和update的優(yōu)先級,即update.lane有關(guān)(只是有關(guān),不一定是由它而來)。得出的任務(wù)優(yōu)先級屬于lanePriority,它不是update的lane,而且與scheduler內(nèi)部的調(diào)度優(yōu)先級是兩個概念,React中的優(yōu)先級轉(zhuǎn)化關(guān)系可以看我總結(jié)過的一篇文章:React中的優(yōu)先級,我們這里只探討任務(wù)優(yōu)先級的生成過程。

在 調(diào)度準(zhǔn)備 的最后提到過,update.lane會被放入root.pendingLanes,隨后會獲取root.pendingLanes中最優(yōu)先級的那些lanes作為renderLanes。任務(wù)優(yōu)先級的生成就發(fā)生在計算renderLanes的階段,任務(wù)優(yōu)先級其實就是renderLanes對應(yīng)的lanePriority。因為renderLanes是本次更新的優(yōu)先級基準(zhǔn),所以它對應(yīng)的lanePriority被作為任務(wù)優(yōu)先級來衡量本次更新任務(wù)的優(yōu)先級權(quán)重理所應(yīng)當(dāng)。

root.pendingLanes,包含了當(dāng)前fiber樹中所有待處理的update的lane。

任務(wù)優(yōu)先級有三類:

  •  同步優(yōu)先級:React傳統(tǒng)的同步渲染模式產(chǎn)生的更新任務(wù)所持有的優(yōu)先級

  •  同步批量優(yōu)先級:同步模式到concurrent模式過渡模式:blocking模式(介紹)產(chǎn)生的更新任務(wù)所持有的優(yōu)先級

  •  concurrent模式下的優(yōu)先級:concurrent模式產(chǎn)生的更新持有的優(yōu)先級

最右面的兩個lane分別為同步優(yōu)先級和同步批量優(yōu)先級,剩下左邊的lane幾乎所有都和concurrent模式有關(guān)。

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;  export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;  concurrent模式下的lanes:/*                               */ 0b1111111111111111111111111111100;

計算renderLanes的函數(shù)是getNextLanes,生成任務(wù)優(yōu)先級的函數(shù)是getHighestPriorityLanes

任務(wù)優(yōu)先級決定著任務(wù)在React中被如何調(diào)度,而由任務(wù)優(yōu)先級轉(zhuǎn)化成的任務(wù)調(diào)度優(yōu)先級(上面給出的scheduler的task結(jié)構(gòu)中的priorityLevel),

決定著Scheduler何時去處理這個任務(wù)。

任務(wù)調(diào)度協(xié)調(diào) - ensureRootIsScheduled

目前為止我們了解了任務(wù)和任務(wù)優(yōu)先級的本質(zhì),下面正式進(jìn)入任務(wù)的調(diào)度過程。React這邊對任務(wù)的調(diào)度本質(zhì)上其實是以任務(wù)優(yōu)先級為基準(zhǔn),去操作多個或單個任務(wù)。

多個任務(wù)的情況,相對于新任務(wù),會對現(xiàn)有任務(wù)進(jìn)行或復(fù)用,或取消的操作,單個任務(wù)的情況,對任務(wù)進(jìn)行或同步,或異步,或批量同步(暫時不需要關(guān)注) 的調(diào)度決策,

這種行為可以看成是一種任務(wù)調(diào)度協(xié)調(diào)機(jī)制,這種協(xié)調(diào)通過ensureRootIsScheduled去實現(xiàn)。

讓我們看一看ensureRootIsScheduled函數(shù)做的事情,先是準(zhǔn)備本次任務(wù)調(diào)度協(xié)調(diào)所需要的lanes和任務(wù)優(yōu)先級,然后判斷是否真的需要調(diào)度

  •  獲取root.callbackNode,即舊任務(wù)

  •  檢查任務(wù)是否過期,將過期任務(wù)放入root.expiredLanes,目的是讓過期任務(wù)能夠以同步優(yōu)先級去進(jìn)入調(diào)度(立即執(zhí)行)

  •  獲取renderLanes(優(yōu)先從root.expiredLanes獲?。绻鹯enderLanes是空的,說明不需要調(diào)度,直接return掉

  •  獲取本次任務(wù),即新任務(wù)的優(yōu)先級:newCallbackPriority

接下來是協(xié)調(diào)任務(wù)調(diào)度的過程:

  •  首先判斷是否有必要發(fā)起一次新調(diào)度,方法是通過比較新任務(wù)的優(yōu)先級和舊任務(wù)的優(yōu)先級是否相等:

    •  相等,則說明無需再次發(fā)起一次調(diào)度,直接復(fù)用舊任務(wù)即可,讓舊任務(wù)在處理更新的時候順便把新任務(wù)給做了。

    •  不相等,則說明新任務(wù)的優(yōu)先級一定高于舊任務(wù),這種情況就是高優(yōu)先級任務(wù)插隊,需要把舊任務(wù)取消掉。

  •  真正發(fā)起調(diào)度,看新任務(wù)的任務(wù)優(yōu)先級:

    •  同步優(yōu)先級:調(diào)用scheduleSyncCallback去同步執(zhí)行任務(wù)。

    •  同步批量執(zhí)行:調(diào)用scheduleCallback將任務(wù)以立即執(zhí)行的優(yōu)先級去加入調(diào)度。

    •  屬于concurrent模式的優(yōu)先級:調(diào)用scheduleCallback將任務(wù)以上面獲取到的新任務(wù)優(yōu)先級去加入調(diào)度。

這里有兩點需要說明:

  1.   為什么新舊任務(wù)的優(yōu)先級如果不相等,那么新任務(wù)的優(yōu)先級一定高于舊任務(wù)?

這是因為每次調(diào)度去獲取任務(wù)優(yōu)先級的時候,都只獲取root.pendingLanes中最緊急的那部分lanes對應(yīng)的優(yōu)先級,低優(yōu)先級的update持有的lane對應(yīng)的優(yōu)先級是無法被獲取到的。通過這種辦法,可以將來自同一事件中的多個更新收斂到一個任務(wù)中去執(zhí)行,言外之意就是同一個事件觸發(fā)的多次更新的優(yōu)先級是一樣的,沒必要發(fā)起多次任務(wù)調(diào)度。例如在一個事件中多次調(diào)用setState:

class Demo extends React.Component {    state = {      count: 0    }    onClick = () => {      this.setState({ count: 1 })      this.setState({ count: 2 })    }    render() {      return <button onClick={onClick}>{this.state.count}</button>    }  }

頁面上會直接顯示出2,雖然onClick事件調(diào)用了兩次setState,但只會引起一次調(diào)度,設(shè)置count為2的那次調(diào)度被因為優(yōu)先級與設(shè)置count為1的那次任務(wù)的優(yōu)先級相同,

所以沒有去再次發(fā)起調(diào)度,而是復(fù)用了已有任務(wù)。這是React17對于多次setState優(yōu)化實現(xiàn)的改變,之前是通過batchingUpdate這種機(jī)制實現(xiàn)的。

  1.  三種任務(wù)優(yōu)先級的調(diào)度模式有何區(qū)別,行為表現(xiàn)上如何?

  • 同步優(yōu)先級:傳統(tǒng)的React同步渲染模式和過期任務(wù)的調(diào)度。通過React提供的scheduleSyncCallback函數(shù)將任務(wù)函數(shù)performSyncWorkOnRoot加入到React自己的同步隊列(syncQueue)中,之后以ImmediateSchedulerPriority的優(yōu)先級將循環(huán)執(zhí)行syncQueue的函數(shù)加入到scheduler中,目的是讓任務(wù)在下一次事件循環(huán)中被執(zhí)行掉。但是因為React的控制,這種模式下的時間片會在任務(wù)都執(zhí)行完之后再去檢查,表現(xiàn)為沒有時間片。

  • 同步批量執(zhí)行:同步渲染模式到concurrent渲染模式的過渡模式blocking模式,會將任務(wù)函數(shù)performSyncWorkOnRoot以ImmediateSchedulerPriority的優(yōu)先級加入到scheduler中,也是讓任務(wù)在下一次事件循環(huán)中被執(zhí)行掉,也不會有時間片的表現(xiàn)。

  • 屬于concurrent模式的優(yōu)先級:將任務(wù)函數(shù)performConcurrentWorkOnRoot以任務(wù)自己的優(yōu)先級加入到scheduler中,scheduler內(nèi)部的會通過這個優(yōu)先級控制該任務(wù)在scheduler內(nèi)部任務(wù)隊列中的排序,從而決定任務(wù)合適被執(zhí)行,而且任務(wù)真正執(zhí)行時會有時間片的表現(xiàn),可以發(fā)揮出scheduler異步可中斷調(diào)度的真正威力。

要注意一點,用來做新舊任務(wù)比較的優(yōu)先級與這里將任務(wù)加入到scheduler中傳入的優(yōu)先級不是一個,后者可由前者通過lanePriorityToSchedulerPriority轉(zhuǎn)化而來。

經(jīng)過以上的分析,相信大家已經(jīng)對ensureRootIsScheduled的運行機(jī)制比較清晰了,現(xiàn)在讓我們看一下它的實現(xiàn):

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {    // 獲取舊任務(wù)    const existingCallbackNode = root.callbackNode;    // 記錄任務(wù)的過期時間,檢查是否有過期任務(wù),有則立即將它放到root.expiredLanes,    // 便于接下來將這個任務(wù)以同步模式立即調(diào)度    markStarvedLanesAsExpired(root, currentTime);    // 獲取renderLanes    const nextLanes = getNextLanes(      root,      root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,    );    // 獲取renderLanes對應(yīng)的任務(wù)優(yōu)先級    const newCallbackPriority = returnNextLanesPriority();    if (nextLanes === NoLanes) {      // 如果渲染優(yōu)先級為空,則不需要調(diào)度      if (existingCallbackNode !== null) {        cancelCallback(existingCallbackNode);        root.callbackNode = null;        root.callbackPriority = NoLanePriority;      }      return;    }    // 如果存在舊任務(wù),那么看一下能否復(fù)用    if (existingCallbackNode !== null) {      // 獲取舊任務(wù)的優(yōu)先級      const existingCallbackPriority = root.callbackPriority;      // 如果新舊任務(wù)的優(yōu)先級相同,則無需調(diào)度      if (existingCallbackPriority === newCallbackPriority) {        return;      }      // 代碼執(zhí)行到這里說明新任務(wù)的優(yōu)先級高于舊任務(wù)的優(yōu)先級      // 取消掉舊任務(wù),實現(xiàn)高優(yōu)先級任務(wù)插隊      cancelCallback(existingCallbackNode);    }    // 調(diào)度一個新任務(wù)    let newCallbackNode;    if (newCallbackPriority === SyncLanePriority) {      // 若新任務(wù)的優(yōu)先級為同步優(yōu)先級,則同步調(diào)度,傳統(tǒng)的同步渲染和過期任務(wù)會走這里      newCallbackNode = scheduleSyncCallback(        performSyncWorkOnRoot.bind(null, root),      );    } else if (newCallbackPriority === SyncBatchedLanePriority) {      // 同步模式到concurrent模式的過渡模式:blocking模式會走這里      newCallbackNode = scheduleCallback(        ImmediateSchedulerPriority,        performSyncWorkOnRoot.bind(null, root),      );    } else {      // concurrent模式的渲染會走這里      // 根據(jù)任務(wù)優(yōu)先級獲取Scheduler的調(diào)度優(yōu)先級      const schedulerPriorityLevel = lanePriorityToSchedulerPriority(        newCallbackPriority,     );      // 計算出調(diào)度優(yōu)先級之后,開始讓Scheduler調(diào)度React的更新任務(wù)      newCallbackNode = scheduleCallback(        schedulerPriorityLevel,        performConcurrentWorkOnRoot.bind(null, root),      );    }    // 更新root上的任務(wù)優(yōu)先級和任務(wù),以便下次發(fā)起調(diào)度時候可以獲取到    root.callbackPriority = newCallbackPriority;    root.callbackNode = newCallbackNode;  }

ensureRootIsScheduled實際上是在任務(wù)調(diào)度層面整合了高優(yōu)先級任務(wù)的插隊和任務(wù)饑餓問題的關(guān)鍵邏輯,這只是宏觀層面的決策,決策背后的原因是React處理更新時

對于不同優(yōu)先級的update的取舍以及對root.pendingLanes的標(biāo)記操作,這需要我們下沉到執(zhí)行更新任務(wù)的過程中。

處理更新

一旦有更新產(chǎn)生,update對象就會被放入updateQueue并掛載到fiber節(jié)點上。構(gòu)建fiber樹時,會帶著renderLanes去處理updateQueue,在beginWork階段,對于類組件

會調(diào)用processUpdateQueue函數(shù),逐個處理這個鏈表上的每個update對象,計算新的狀態(tài),一旦update持有的優(yōu)先級不夠,那么就會跳過這個update的處理,并把這個被跳過的update的lane放到fiber.lanes中,好在completeWork階段收集起來。

循環(huán)updateQueue去計算狀態(tài)的過程實際上較為復(fù)雜,因為低優(yōu)先級update會被跳過并且會重做,所以這涉及到最終狀態(tài)統(tǒng)一的問題,關(guān)于這一過程的原理解讀在我的這篇文章里:扒一扒React計算狀態(tài)的原理,在本篇文章中只關(guān)注優(yōu)先級相關(guān)的部分。

關(guān)于優(yōu)先級的部分比較好理解,就是只處理優(yōu)先級足夠的update,跳過那些優(yōu)先級不足的update,并且將這些update的lane放到fiber.lanes中。我們直接來看一下實現(xiàn):

function processUpdateQueue<State>(    workInProgress: Fiber,    props: any,    instance: any,    renderLanes: Lanes,  ): void {    ...    if (firstBaseUpdate !== null) {      let update = firstBaseUpdate;      do {        const updateupdateLane = update.lane;        // isSubsetOfLanes函數(shù)的意義是,判斷當(dāng)前更新的優(yōu)先級(updateLane)        // 是否在渲染優(yōu)先級(renderLanes)中如果不在,那么就說明優(yōu)先級不足        if (!isSubsetOfLanes(renderLanes, updateLane)) {          ...          /*          *          * newLanes會在最后被賦值到workInProgress.lanes上,而它又最終          * 會被收集到root.pendingLanes。          *          * 再次更新時會從root上的pendingLanes中找出應(yīng)該在本次中更新的優(yōu)先          * 級(renderLanes),renderLanes含有本次跳過的優(yōu)先級,再次進(jìn)入,          * processUpdateQueue wip的優(yōu)先級符合要求,被更新掉,低優(yōu)先級任務(wù)          * 因此被重做          * */          newLanes = mergeLanes(newLanes, updateLane);        } else {          // 優(yōu)先級足夠,去計算state          ...       }      } while (true);      // 將newLanes賦值給workInProgress.lanes,      // 就是將被跳過的update的lane放到fiber.lanes      workInProgress.lanes = newLanes;   }  }

只處理優(yōu)先級足夠的update是讓高優(yōu)先級任務(wù)被執(zhí)行掉的最本質(zhì)原因,在循環(huán)了一次updateQueue之后,那些被跳過的update的lane又被放入了fiber.lanes,現(xiàn)在,只需要將它放到root.pendingLanes中,就能表示在本輪更新后,仍然有任務(wù)未被處理,從而實現(xiàn)低優(yōu)先級任務(wù)被重新調(diào)度。所以接下來的過程就是fiber節(jié)點的完成階段:completeWork階段去收集這些lanes。

收集未被處理的lane

在completeUnitOfWork的時候,fiber.lanes 和 childLanes被一層一層收集到父級fiber的childLanes中,該過程發(fā)生在completeUnitOfWork函數(shù)中調(diào)用的resetChildLanes,它循環(huán)fiber節(jié)點的子樹,將子節(jié)點及其兄弟節(jié)點中的lanes和childLanes收集到當(dāng)前正在complete階段的fiber節(jié)點上的childLanes。

假設(shè)第3層中的<List/>和<Table/>組件都分別有update因為優(yōu)先級不夠而被跳過,那么在它們父級的div fiber節(jié)點completeUnitOfWork的時候,會調(diào)用resetChildLanes

把它倆的lanes收集到div fiber.childLanes中,最終把所有的lanes收集到root.pendingLanes. 

                               root(pendingLanes: 0b01110)                                     |  1                                  App                                     |                                     |  2 compeleteUnitOfWork-----------> div (childLanes: 0b01110)                                     /                                    /  3                              <List/> ---------> <Table/> --------> p                            (lanes: 0b00010)   (lanes: 0b00100)                         (childLanes: 0b01000)       /                                 /                   /                                /                   /  4                            p                   ul                                                  /                                                 /                                                li ------> li

在每一次往上循環(huán)的時候,都會調(diào)用resetChildLanes,目的是將fiber.childLanes層層收集。

function completeUnitOfWork(unitOfWork: Fiber): void {    // 已經(jīng)結(jié)束beginWork階段的fiber節(jié)點被稱為completedWork    let completedWork = unitOfWork;    do {      // 向上一直循環(huán)到root的過程      ...      // fiber節(jié)點的.flags上沒有Incomplete,說明是正常完成了工作      if ((completedWork.flags & Incomplete) === NoFlags) {        ...        // 調(diào)用resetChildLanes去收集lanes        resetChildLanes(completedWork);        ...      } else {/*...*/}      ...    } while (completedWork !== null);    ...  }

resetChildLanes中只收集當(dāng)前正在complete的fiber節(jié)點的子節(jié)點和兄弟節(jié)點的lanes以及childLanes:

function resetChildLanes(completedWork: Fiber) {    ...    let newChildLanes = NoLanes;    if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {      // profile相關(guān),無需關(guān)注    } else {      // 循環(huán)子節(jié)點和兄弟節(jié)點,收集lanes      let child = completedWork.child;      while (child !== null) {        // 收集過程        newChildLanes = mergeLanes(          newChildLanes,          mergeLanes(child.lanes, child.childLanes),        );        childchild = child.sibling;      }    }    // 將收集到的lanes放到該fiber節(jié)點的childLanes中    completedWork.childLanes = newChildLanes;  }

最后將這些收集到的childLanes放到root.pendingLanes的過程,是發(fā)生在本次更新的commit階段中,因為render階段的渲染優(yōu)先級來自root.pendingLanes,不能隨意地修改它。所以要在render階段之后的commit階段去修改。我們看一下commitRootImpl中這個過程的實現(xiàn):

function commitRootImpl(root, renderPriorityLevel) {    // 將收集到的childLanes,連同root自己的lanes,一并賦值給remainingLanes    let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);    // markRootFinished中會將remainingLanes賦值給remainingLanes    markRootFinished(root, remainingLanes);    ...  }

重新發(fā)起調(diào)度

至此,我們將低優(yōu)先級任務(wù)的lane重新收集到了root.pendingLanes中,這時只需要再發(fā)起一次調(diào)度就可以了,通過在commit階段再次調(diào)用ensureRootIsScheduled去實現(xiàn),這樣就又會走一遍調(diào)度的流程,低優(yōu)先級任務(wù)被執(zhí)行。

function commitRootImpl(root, renderPriorityLevel) {    // 將收集到的childLanes,連同root自己的lanes,一并賦值給remainingLanes    let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);    // markRootFinished中會將remainingLanes賦值給remainingLanes    markRootFinished(root, remainingLanes);    ...    // 在每次所有更新完成的時候都會調(diào)用這個ensureRootIsScheduled    // 以保證root上任何的pendingLanes都能被處理    ensureRootIsScheduled(root, now());  }

總結(jié)

高優(yōu)先級任務(wù)插隊,低優(yōu)先級任務(wù)重做的整個過程共有四個關(guān)鍵點:

  •  ensureRootIsScheduled取消已有的低優(yōu)先級更新任務(wù),重新調(diào)度一個任務(wù)去做高優(yōu)先級更新,并以root.pendingLanes中最重要的那部分lanes作為渲染優(yōu)先級

  •  執(zhí)行更新任務(wù)時跳過updateQueue中的低優(yōu)先級update,并將它的lane標(biāo)記到fiber.lanes中。

  •  fiber節(jié)點的complete階段收集fiber.lanes到父級fiber的childLanes,一直到root。

  •  commit階段將所有root.childLanes連同root.lanes一并賦值給root.pendingLanes。

  •  commit階段的最后重新發(fā)起調(diào)度。

整個流程始終以高優(yōu)先級任務(wù)為重,顧全大局,最能夠體現(xiàn)React提升用戶體驗的決心。

到此,相信大家對“如何理解React中的高優(yōu)先級任務(wù)插隊機(jī)制”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI