自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React 的調(diào)度系統(tǒng) Scheduler

開(kāi)發(fā) 前端
首先是 Scheduler 底層大多數(shù)情況下會(huì)使用 MessageChannel,作為循環(huán)執(zhí)行異步任務(wù)的能力。通過(guò)它來(lái)不斷地執(zhí)行任務(wù)隊(duì)列中的任務(wù)。

React 使用了全新的 Fiber 架構(gòu),將原本需要一次性遞歸找出所有的改變,并一次性更新真實(shí) DOM 的流程,改成通過(guò)時(shí)間分片,先分成一個(gè)個(gè)小的異步任務(wù)在空閑時(shí)間找出改變,最后一次性更新 DOM。

這里需要使用調(diào)度器,在瀏覽器空閑的時(shí)候去做這些異步小任務(wù)。

Scheduler

做這個(gè)調(diào)度工作的在 React 中叫做 Scheduler(調(diào)度器)模塊。

其實(shí)瀏覽器是提供一個(gè) requestIdleCallback 的方法,讓我們可以在瀏覽器空閑的時(shí)去調(diào)用傳入去的回調(diào)函數(shù)。但因?yàn)榧嫒菪圆缓?,給的優(yōu)先級(jí)可能太低,執(zhí)行是在渲染幀執(zhí)行等缺點(diǎn)。

所以 React 實(shí)現(xiàn)了 requestIdleCallback 的替代方案,也就是這個(gè) Scheduler。它的底層是 基于 MessageChannel 的。

為什么是 MessageChannel?

選擇 MessageChannel 的原因,是首先異步得是個(gè)宏任務(wù),因?yàn)楹耆蝿?wù)中會(huì)在下次事件循環(huán)中執(zhí)行,不會(huì)阻塞當(dāng)前頁(yè)面的更新。MessageChannel 是一個(gè)宏任務(wù)。

沒(méi)選常見(jiàn)的 setTimeout,是因?yàn)镸essageChannel 能較快執(zhí)行,在 0~1ms 內(nèi)觸發(fā),像 setTimeout 即便設(shè)置 timeout 為 0 還是需要 4~5ms。相同時(shí)間下,MessageChannel 能夠完成更多的任務(wù)。

若瀏覽器不支持 MessageChannel,還是得降級(jí)為 setTimeout。

其實(shí)如果 setImmediate 存在的話,會(huì)優(yōu)先使用 setImmediate,但它只在少量環(huán)境(比如 IE 的低版本、Node.js)中存在。

邏輯是在 packages/scheduler/src/forks/Scheduler.js 中實(shí)現(xiàn)的:

// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom

/***** 異步選擇策略 *****/
// 【1】 優(yōu)先使用 setImmediate
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
schedulePerformWorkUntilDeadline = () {
localSetImmediate(performWorkUntilDeadline);
};
}
// 【2】 然后是 MessageChannel
else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () {
port.postMessage(null);
};
}
// 【3】 最后是 setTimeout(兜底)
else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () {
localSetTimeout(performWorkUntilDeadline, 0);
};
}

另外,也沒(méi)有選擇使用 requestAnimationFrame,是因?yàn)樗臋C(jī)制比較特別,是在更新頁(yè)面前執(zhí)行,但更新頁(yè)面的時(shí)機(jī)并沒(méi)有規(guī)定,執(zhí)行時(shí)機(jī)并不穩(wěn)定。

底層的異步循環(huán)

requestHostCallback 方法,用于請(qǐng)求宿主(指瀏覽器)去執(zhí)行函數(shù)。該方法會(huì)將傳入的函數(shù)保存起來(lái)到 scheduledHostCallback 上,

然后調(diào)用 schedulePerformWorkUntilDeadline 方法。

schedulePerformWorkUntilDeadline 方法一調(diào)用,就停不下來(lái)了。

它會(huì)異步調(diào)用 performWorkUntilDeadline,后者又調(diào)用回 schedulePerformWorkUntilDeadline,最終實(shí)現(xiàn) 不斷地異步循環(huán)執(zhí)行 performWorkUntilDeadline。

// 請(qǐng)求宿主(指瀏覽器)執(zhí)行函數(shù)
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}

isMessageLoopRunning 是一個(gè) flag,表示是否正在走循環(huán)。防止同一時(shí)間調(diào)用多次 schedulePerformWorkUntilDeadline。

React 會(huì)調(diào)度 workLoopSync / workLoopConcurrent

我們?cè)?React 項(xiàng)目啟動(dòng)后,執(zhí)行一個(gè)更新操作,會(huì)調(diào)用 ensureRootIsScheduled 方法。

function ensureRootIsScheduled(root, currentTime) {
// 最高優(yōu)先級(jí)
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
// Legacy Mode,即 ReactDOM.render() 啟用的同步模式
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// 立即執(zhí)行優(yōu)先級(jí),去清空需要同步執(zhí)行的任務(wù)
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
} else {
// 初始化 schedulerPriorityLevel 并計(jì)算出 Scheduler 支持的優(yōu)先級(jí)值
let schedulerPriorityLevel;
// ...

scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root), // 并發(fā)模式
);
}
}

該方法有很多分支,最終會(huì)根據(jù)條件調(diào)用:

  1. performSyncWorkOnRoot(立即執(zhí)行)
  2. performConcurrentWorkOnRoot(并發(fā)執(zhí)行,且會(huì)用 scheduler 的 scheduleCallback 進(jìn)行異步調(diào)用)

performSyncWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopSync 方法:

// 調(diào)用鏈路:
// performSyncWorkOnRoot -> renderRootSync -> workLoopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}

workInProgress 表示一個(gè)需要進(jìn)行處理的 FiberNode。

performUnitOfWork 方法用于處理一個(gè) workInProgress,進(jìn)行調(diào)和操作,計(jì)算出新的 fiberNode。

同樣,performConcurrentWorkOnRoot 最終會(huì)執(zhí)行重要的 workLoopConcurrent 方法。

// 調(diào)用鏈路:
// performConcurrentWorkOnRoot -> performConcurrentWorkOnRoot -> renderRootConcurrent
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}

和 workLoopSync 很相似,但循環(huán)條件里多了一個(gè)來(lái)自 Scheduler 的 shouldYield() 決定是否將進(jìn)程讓出給瀏覽器,這樣就能做到中斷 Fiber 的調(diào)和階段,做到時(shí)間分片。

scheduleCallback

上面的 workLoopSync 和 workLoopConcurrent 都是通過(guò) scheduleCallback 去調(diào)度的。

scheduleCallback 方法傳入優(yōu)先級(jí) priorityLevel、需要指定的回調(diào)函數(shù) callback ,以及一個(gè)可選項(xiàng) options。

scheduleCallback 的實(shí)現(xiàn)如下(做了簡(jiǎn)化):

function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();

var startTime;
if (options?.delay) {
startTime = currentTime + options.delay;
}
// 有效期時(shí)長(zhǎng),根據(jù)優(yōu)先級(jí)設(shè)置。
var timeout;
// ...
// 計(jì)算出 過(guò)期時(shí)間點(diǎn)
var expirationTime = startTime + timeout;

// 創(chuàng)建一個(gè)任務(wù)
var newTask = {
id: taskIdCounter++,
callback, // 這個(gè)就是任務(wù)本身
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};

// 說(shuō)明新任務(wù)是加了 option.delay 的任務(wù),需要延遲執(zhí)行
// 我們會(huì)放到未逾期隊(duì)列(timerQueue)中
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 沒(méi)有需要逾期的任務(wù),且優(yōu)先級(jí)最高的未逾期任務(wù)就是這個(gè)新任務(wù)
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 那,用 setTimeout 延遲 options.delay 執(zhí)行 handleTimeout
requestHostTimeout(handleTimeout, startTime - currentTime);
}
}
// 立即執(zhí)行的任務(wù),加入到逾期隊(duì)列(taskQueue)
else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);

// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
}

push / peek / pop 這些是 scheduler 提供的操作 優(yōu)先級(jí)隊(duì)列 的操作方法。

優(yōu)先級(jí)隊(duì)列的底層實(shí)現(xiàn)是小頂堆,實(shí)現(xiàn)原理不展開(kāi)講。我們只需要記住優(yōu)先級(jí)隊(duì)列的特性:就是出隊(duì)的時(shí)候,會(huì)取優(yōu)先級(jí)最高的任務(wù)。在 scheduler 中,sortIndex 最小的任務(wù)的優(yōu)先級(jí)最高。

push(queue, task)? 表示入隊(duì),加一個(gè)新任務(wù);peek(queue)? 表示得到最高優(yōu)先級(jí)(不出隊(duì));pop(queue) 表示將最高優(yōu)先級(jí)任務(wù)出隊(duì)。

taskQueue 為逾期的任務(wù)隊(duì)列,需要趕緊執(zhí)行。新生成的任務(wù)(沒(méi)有設(shè)置 options.delay)會(huì)放到 taskQueue,并以 expirationTime 作為優(yōu)先級(jí)(sortIndex)來(lái)比較。

timerQueue 是還沒(méi)逾期的任務(wù)隊(duì)列,以 startTime 作為優(yōu)先級(jí)來(lái)比較。如果逾期了,就會(huì) 取出放到 taskQueue 里。

handleTimeout

// 如果沒(méi)有逾期的任務(wù),且優(yōu)先級(jí)最高的未逾期任務(wù)就是這個(gè)新任務(wù)
// 延遲執(zhí)行 handleTimeout
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
requestHostTimeout(handleTimeout, startTime - currentTime);
}

requestHostTimeout 其實(shí)就是 setTimeout 定時(shí)器的簡(jiǎn)單封裝,在 newTask 過(guò)期的時(shí)間點(diǎn)(startTime - currentTime 后)執(zhí)行 handleTimeout。

function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); // 更新 timerQueue 和 taskQueue

if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) { // 有要執(zhí)行的逾期任務(wù)
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 清空 taskQueue 任務(wù)
} else { // 沒(méi)有逾期任務(wù)
const firstTimer = peek(timerQueue);
if (firstTimer !== null) { // 但有未逾期任務(wù),用 setTimeout 晚點(diǎn)再調(diào)用自己
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}

handleTimeout 下會(huì)調(diào)用 advanceTimers 方法,根據(jù)當(dāng)前時(shí)間要將 timerTask 中逾期的任務(wù)搬到 taskQueue 下。

(advanceTimers 這個(gè)方法會(huì)在多個(gè)位置被調(diào)用。搬一搬,更健康)

搬完后,看看 taskQueue 有沒(méi)有任務(wù)要做,有的話就調(diào)用 flushWork 清空 taskQueue 任務(wù)。沒(méi)有的話看看有沒(méi)有未逾期任務(wù),用定時(shí)器在它過(guò)期的時(shí)間點(diǎn)再遞歸執(zhí)行 handleTimeout。

workLoop

flushWork 會(huì) 調(diào)用 workLoop。flushWork 還需要做一些額外的修改模塊文件變量的操作。

function flushWork(hasTimeRemaining, initialTime) {
// ...
return workLoop(hasTimeRemaining, initialTime);
}

workLoop  會(huì)不停地從 taskQueue 取出任務(wù)來(lái)執(zhí)行。其核心邏輯為:

function workLoop(hasTimeRemaining, initialTime) {
// 更新 taskQueue,并取出一個(gè)任務(wù)
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);

while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// 執(zhí)行任務(wù)
const callback = currentTask.callback;
callback();

// 更新 taskQueue,并取出一個(gè)任務(wù)
currentTime = getCurrentTime();
advanceTimers(currentTime);
currentTask = peek(taskQueue);
}
return currentTask !== null;
}

shouldYieldToHost

上面的循環(huán)并不是一直會(huì)執(zhí)行到 currentTask 為 null 為止,在必要的時(shí)候還是會(huì)跳出的。我們是通過(guò) shouldYieldToHost 方法判斷是否要跳出。

此外,F(xiàn)iber 異步更新的 workLoopConcurrent 方法用到的 shouldYield,其實(shí)就是這個(gè) shouldYieldToHost。

shouldYieldToHost 核心實(shí)現(xiàn):

const frameYieldMs = 5;
var frameInterval = frameYieldMs;

function shouldYieldToHost() {
var timeElapsed = getCurrentTime() - startTime;
// 經(jīng)過(guò)的時(shí)間小于 5 ms,不需要讓出進(jìn)程
if (timeElapsed < frameInterval) {
return false;
}
return true;
}

export {
// 會(huì)重命名為 unstable_shouldYield 導(dǎo)出
shouldYieldToHost as unstable_shouldYield,
}

計(jì)算經(jīng)過(guò)的時(shí)間,如果小于幀間隔時(shí)間(frameInterval,通常為 5ms),不需要讓出進(jìn)程,否則讓出。

startTime 是模塊文件的最外層變量,會(huì)在 performWorkUntilDeadline 方法中賦值,也就是任務(wù)開(kāi)始調(diào)度的時(shí)候。

流程圖

試著畫一下 Scheduler 的調(diào)度流程圖。

圖片

結(jié)尾

Scheduler 一套下來(lái)還是挺復(fù)雜的。

首先是 Scheduler 底層大多數(shù)情況下會(huì)使用 MessageChannel,作為循環(huán)執(zhí)行異步任務(wù)的能力。通過(guò)它來(lái)不斷地執(zhí)行任務(wù)隊(duì)列中的任務(wù)。

任務(wù)隊(duì)列是特殊的優(yōu)先級(jí)隊(duì)列,特性是出隊(duì)時(shí),拿到優(yōu)先級(jí)最高的任務(wù)(在 Scheduler 中對(duì)比的是 sortIndex,值是一個(gè)時(shí)間戳)。

任務(wù)隊(duì)列在 Scheduler 中有兩種。一種是逾期任務(wù) taskQueue,需要趕緊執(zhí)行,另一種是延期任務(wù) timerQueue,還不到時(shí)間執(zhí)行。Scheduler 會(huì)根據(jù)當(dāng)前時(shí)間,將逾期的 timerQueue 任務(wù)放到 taskQueue 中,然后從 taskQueue 取出優(yōu)先級(jí)最高的任務(wù)去執(zhí)行。

Scheduler 向外暴露 scheduleCallback 方法,該方法接受一個(gè)優(yōu)先級(jí)和一個(gè)函數(shù)(就是任務(wù)),對(duì)于 React 來(lái)說(shuō),它通常是 workLoopSync 或 workLoopConcurrent。

scheduleCallback 會(huì)設(shè)置新任務(wù)的過(guò)期時(shí)間(根據(jù)優(yōu)先級(jí)),并判斷是否為延時(shí)任務(wù)(根據(jù) options.delay)決定放入哪個(gè)任務(wù)隊(duì)列中。然后啟用循環(huán)執(zhí)行異步任務(wù),不斷地清空?qǐng)?zhí)行 taskQueue。

Scheduler 也向外暴露了 shouldYield,通過(guò)它可以知道是否執(zhí)行時(shí)間過(guò)長(zhǎng),應(yīng)該讓出進(jìn)程給瀏覽器。該方法同時(shí)也在 Scheduler 內(nèi)部的循環(huán)執(zhí)行異步任務(wù)中作為一種打斷循環(huán)的判斷條件。

React 的并發(fā)模式下,可以用它作為暫停調(diào)和階段的依據(jù)。

責(zé)任編輯:姜華 來(lái)源: 前端西瓜哥
相關(guān)推薦

2021-01-29 08:22:03

調(diào)度器Yarn架構(gòu)

2017-08-23 11:10:44

Kubernetes 調(diào)度詳解

2024-03-22 07:48:51

ReactScheduler底層調(diào)度器

2021-10-27 06:55:18

ReacFiber架構(gòu)

2022-08-19 18:03:12

Scheduler

2021-02-01 11:30:13

React前端調(diào)度

2022-08-27 22:36:18

Kubernetes調(diào)度器

2021-12-16 06:21:16

React組件前端

2022-01-10 08:31:29

React組件前端

2023-03-03 15:37:32

GMP 模型goroutine

2010-07-29 11:34:31

多媒體調(diào)度系統(tǒng)MDS捷思銳

2021-12-26 12:10:21

React組件前端

2023-03-06 00:27:02

Kubernetesscheduler系統(tǒng)

2021-04-12 06:04:30

React操作系統(tǒng)Reconciler

2018-05-30 13:42:39

2012-05-14 14:09:53

Linux內(nèi)核調(diào)度系統(tǒng)

2024-09-02 18:10:20

2022-02-08 12:30:30

React事件系統(tǒng)React事件系統(tǒng)

2022-03-15 10:20:00

云原生系統(tǒng)實(shí)踐

2023-11-29 09:29:48

Kuberneteskube
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)