創(chuàng)建更新之后,找到 Root 然后進(jìn)入調(diào)度哗咆,同步和異步操作完全不同咽扇,實(shí)現(xiàn)更新分片的性能優(yōu)化。
主流的瀏覽器刷新頻率為 60Hz洁段,即每(1000ms / 60Hz)16.6ms 瀏覽器刷新一次应狱。JS可以操作 DOM,JS線程
與 GUI渲染線程
是互斥的祠丝。所以 **JS腳本執(zhí)行 **和 **瀏覽器布局疾呻、繪制 **不能同時(shí)執(zhí)行。
在每16.6ms時(shí)間內(nèi)写半,需要完成如下工作:
JS腳本執(zhí)行 ----- 樣式布局 ----- 樣式繪制
既然以瀏覽器是否有剩余時(shí)間作為任務(wù)中斷的標(biāo)準(zhǔn)岸蜗,那么我們需要一種機(jī)制,當(dāng)瀏覽器在每一幀 16.6ms 中執(zhí)行完自己的 GUI 渲染線程后叠蝇,還有剩余時(shí)間的話能通知我們執(zhí)行 react 的異步更新任務(wù)璃岳,react 執(zhí)行時(shí)會(huì)自己計(jì)時(shí),如果時(shí)間到了悔捶,而 react 依然沒有執(zhí)行完铃慷,則會(huì)掛起自己,并把控制權(quán)還給瀏覽器蜕该,以便瀏覽器執(zhí)行更高優(yōu)先級(jí)的任務(wù)犁柜。然后 react 在下次瀏覽器空閑時(shí)恢復(fù)執(zhí)行。而如果是同步任務(wù)堂淡,則不會(huì)中斷馋缅,會(huì)一直占用瀏覽器直到頁面渲染完畢坛怪。
其實(shí)部分瀏覽器已經(jīng)實(shí)現(xiàn)了這個(gè)API,這就是 requestIdleCallback(字面意思:請求空閑回調(diào))股囊。但是由于以下因素袜匿,React 放棄使用:
- 瀏覽器兼容性
- 觸發(fā)頻率不穩(wěn)定,受很多因素影響稚疹。比如當(dāng)我們的瀏覽器切換tab后居灯,之前tab注冊的 requestIdleCallback 觸發(fā)的頻率會(huì)變得很低。
React 實(shí)現(xiàn)了功能更完備的 requestIdleCallback polyfill(使用window.requestAnimationFrame()
和 JavaScript 任務(wù)隊(duì)列進(jìn)行模擬)内狗,這就是 Scheduler怪嫌,除了在空閑時(shí)觸發(fā)回調(diào)的功能外,Scheduler 還提供了多種調(diào)度優(yōu)先級(jí)供任務(wù)設(shè)置柳沙。
當(dāng) Scheduler 將任務(wù)交給 Reconciler 后岩灭,Reconciler 會(huì)為變化的虛擬 DOM 打上代表增/刪/更新的標(biāo)記,類似這樣:
// 這種二進(jìn)制存儲(chǔ)數(shù)據(jù):
// 設(shè)置:集合 | 目標(biāo)
// 查詢:集合 & 目標(biāo)
// 取消:集合 & ~目標(biāo)
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
整個(gè) Scheduler 與 Reconciler 的工作都在內(nèi)存中進(jìn)行赂鲤。只有當(dāng)所有組件都完成 Reconciler 的工作后噪径,才會(huì)統(tǒng)一交給 Renderer。
Renderer 根據(jù) Reconciler 為虛擬 DOM 打的標(biāo)記数初,同步執(zhí)行對應(yīng)的 DOM 操作找爱。
其中紅框中的步驟隨時(shí)可能由于以下原因被中斷:
- 有其他更高優(yōu)任務(wù)需要先更新
- 當(dāng)前幀沒有剩余時(shí)間
由于紅框中的工作都在內(nèi)存中進(jìn)行,不會(huì)更新頁面上的DOM泡孩,即使反復(fù)中斷用戶也不會(huì)看見更新不完全的DOM车摄。
因此可以說 Scheduler 和 Reconciler 是和平臺(tái)無關(guān)的,而和平臺(tái)相關(guān)的是 Renderer仑鸥。
大致更新調(diào)度的流程:
- 首先通過 ReactDOM.render/setState/forceUpdate 產(chǎn)生更新吮播。
- 找到產(chǎn)生更新的節(jié)點(diǎn)所對應(yīng)的 FiberRoot,將其加入到調(diào)度器中(多次調(diào)用 ReactDOM.render 就會(huì)有多個(gè) root)眼俊。
- 根據(jù) expirationTime 判斷是同步更新意狠,還是異步更新。具體就是在上一篇提到的泵琳,在
computeExpirationForFiber()
計(jì)算過期時(shí)間時(shí)摄职,根據(jù)fiber.mode & ConcurrentMode
模式是否開啟誊役,來計(jì)算同步的過期時(shí)間获列,和異步的過期時(shí)間,也就是同步更新任務(wù)
和異步更新任務(wù)
蛔垢。同步更新任務(wù)沒有 deadline(用于 react 執(zhí)行的分片時(shí)間)击孩,會(huì)立即執(zhí)行,不會(huì)被中斷鹏漆。而異步更新任務(wù)有 deadline巩梢,需要等待調(diào)度创泄,并可能會(huì)中斷。在此過程中括蝠,同步任務(wù)和異步任務(wù)最終會(huì)匯聚到一起鞠抑,根據(jù)是否有 deadline,會(huì)進(jìn)入同步循環(huán)條件或異步循環(huán)條件忌警,而這個(gè)循環(huán)就是指遍歷整棵 Fiber 樹的每個(gè)節(jié)點(diǎn)進(jìn)行更新的操作搁拙。同步任務(wù)遍歷完更新完就完了,而異步任務(wù)在更新節(jié)點(diǎn)時(shí)會(huì)受到分片調(diào)度的控制法绵。 - 異步更新任務(wù)被加入到 Scheduler 的 callbackList 中等待調(diào)度箕速。調(diào)度時(shí),會(huì)檢查是否有任務(wù)已經(jīng)過期(expirationTime)朋譬,會(huì)先把所有已經(jīng)超時(shí)的任務(wù)執(zhí)行掉盐茎,直到遇到非超時(shí)的任務(wù)時(shí),如果當(dāng)前時(shí)間分片 deadline 還沒到點(diǎn)徙赢,則繼續(xù)執(zhí)行字柠,如果已經(jīng)到點(diǎn)了,則控制權(quán)交給瀏覽器狡赐。
- 采用了 deadline 分片保證異步更新任務(wù)不會(huì)阻塞瀏覽器 GUI 的渲染募谎、如動(dòng)畫等能在 30FPS 以上。
TODO:Scheduler 圖阴汇,可能要自己手繪
2. scheduleWork
在上一篇 “React 中的更新” 提到数冬,ReactDOM.render/setState/forceUpdate 最終都會(huì)進(jìn)入 scheduleWork 即調(diào)度工作。
- 找到更新所對應(yīng)的 FiberRoot 節(jié)點(diǎn)搀庶。
- 如果符合條件則重置 stack
- 如果符合條件就請求工作調(diào)度
TODO: Fiber 樹圖
**
點(diǎn)擊 button拐纱,是 List 組件實(shí)例調(diào)用了 setState,創(chuàng)建 update 后開始調(diào)度時(shí)哥倔,是將 List 所在的 RootFiber 這個(gè) fiber 根節(jié)點(diǎn)加入到調(diào)度隊(duì)列中(而并不是直接把 List Fiber 節(jié)點(diǎn)加入調(diào)度中)秸架,正如每次更新時(shí)也是從 RootFiber 開始更新。
一些全局變量:
- isWorking: 用來標(biāo)志是否當(dāng)前有更新正在進(jìn)行咆蒿,不區(qū)分階段东抹,包含了 commit 和 render 階段
- isCommitting: 是否處于 commit 階段
React在16版本之后處理任務(wù)分為兩個(gè)階段:
- render 階段: 判斷哪些變更需要被處理成 DOM,也就是比較上一次渲染的結(jié)果和新的更新沃测,打標(biāo)記缭黔。
- commit 階段: 處理從 js 對象到 DOM 的更新,不會(huì)被打斷蒂破,并且會(huì)調(diào)用 componentDidMount 和 componentDidUpdate 這些生命周期方法馏谨。
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}
// 異步任務(wù)由于時(shí)間片不夠被中斷,執(zhí)行權(quán)了交給瀏覽器附迷,
// 此時(shí)產(chǎn)生了更新任務(wù)惧互,其優(yōu)先級(jí)更高哎媚,則會(huì)打斷老的任務(wù)。
if (
!isWorking &&
nextRenderExpirationTime !== NoWork &&
expirationTime < nextRenderExpirationTime
) {
// This is an interruption. (Used for performance tracking.)
interruptedBy = fiber; // 給開發(fā)工具用的喊儡,用來展示被哪個(gè)節(jié)點(diǎn)打斷了異步任務(wù)
resetStack(); // 重置之前的中斷的任務(wù)已經(jīng)產(chǎn)生的部分節(jié)點(diǎn)更新
}
// 暫時(shí)先忽略
markPendingPriorityLevel(root, expirationTime);
// 沒有正在進(jìn)行的工作拨与,或者是上一次render階段已經(jīng)結(jié)束(更新結(jié)束),已經(jīng)到了commit階段艾猜,
// 那么可以繼續(xù)請求一次調(diào)度工作截珍。
if (
// If we're in the render phase, we don't need to schedule this root
// for an update, because we'll do it before we exit...
!isWorking ||
isCommitting ||
// ...unless this is a different root than the one we're rendering.
nextRoot !== root // 單個(gè)FiberRoot入口時(shí)nextRoot永遠(yuǎn)等于root,所以基本不需要考慮這個(gè)條件箩朴,因?yàn)榛径际菃稳肟趹?yīng)用
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime); // 請看下一節(jié)
}
// 檢測死循環(huán)更新
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// Reset this back to zero so subsequent updates don't throw.
nestedUpdateCount = 0;
invariant(
false,
'Maximum update depth exceeded. This can happen when a ' +
'component repeatedly calls setState inside ' +
'componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
}
}
function resetStack() {
// 用于記錄render階段Fiber樹遍歷過程中下一個(gè)需要執(zhí)行的節(jié)點(diǎn)岗喉,
// 不為null說明之前存在更新任務(wù),只是時(shí)間片不夠了炸庞,被中斷了
// 于是就要向上遍歷父節(jié)點(diǎn)钱床,將已經(jīng)更新了的狀態(tài)回退到更新之前,
// 回退是為了避免狀態(tài)混亂埠居,不能把搞了一半的攤子直接丟給新的更新任務(wù),
// 因?yàn)樾碌母邇?yōu)先級(jí)任務(wù)也要從根節(jié)點(diǎn)RootFiber開始更新查牌。
if (nextUnitOfWork !== null) {
let interruptedWork = nextUnitOfWork.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
checkThatStackIsEmpty();
}
// 回退一些全局變量
nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestAbsoluteTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
}
scheduleWorkToRoot()
根據(jù)傳入的 Fiber 節(jié)點(diǎn),找到對應(yīng)的 FiberRoot(也就是最初調(diào)用 ReactDOM.render 時(shí)創(chuàng)建的 FiberRoot)滥壕。對于 ReactDOM.render 產(chǎn)生的調(diào)用 scheduleWork()
纸颜,其傳入的是 RootFiber 節(jié)點(diǎn), RootFiber.stateNode
就找到了 FiberRoot绎橘;而 setState/forceUpdate 傳入的是自身組件所對應(yīng)的 Fiber 子節(jié)點(diǎn)胁孙,會(huì)復(fù)雜一些:
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
// 記錄調(diào)度時(shí)間,
recordScheduleUpdate();
// 如果尚未更新過称鳞,沒有存儲(chǔ)過期時(shí)間涮较,或者最新計(jì)算出的過期時(shí)間更短,意味著優(yōu)先級(jí)更高冈止,
// 那么就更新目標(biāo)Fiber上的過期時(shí)間 (即產(chǎn)生更新的那個(gè)Fiber狂票,如剛才的List組件對應(yīng)的Fiber)
// Update the source fiber's expiration time
if (
fiber.expirationTime === NoWork ||
fiber.expirationTime > expirationTime
) {
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
// 同理如果alternate fiber(之前說過的雙緩存、雙buff機(jī)制)存在熙暴,也要嘗試更新過期時(shí)間
if (
alternate !== null &&
(alternate.expirationTime === NoWork ||
alternate.expirationTime > expirationTime)
) {
alternate.expirationTime = expirationTime;
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return; // fiber.return指向的就是父fiber節(jié)點(diǎn)闺属。
let root = null;
// 只有RootFiber.return會(huì)是null,說明傳入的Fiber就是RootFiber周霉,其tag就是3掂器,也就是HostRoot
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode; // 找到了 FiberRoot
} else {
// 向上遍歷,尋找FiberRoot
while (node !== null) {
alternate = node.alternate;
if (
node.childExpirationTime === NoWork ||
node.childExpirationTime > expirationTime
) {
// childExpirationTime: 父節(jié)點(diǎn)記錄了子樹中優(yōu)先級(jí)最高的過期時(shí)間诗眨,即最先的過期時(shí)間
// 如果當(dāng)前傳入節(jié)點(diǎn)的過期時(shí)間優(yōu)先級(jí)更高唉匾,則更新父節(jié)點(diǎn)的childExpirationTime,
// 同理還要更新父節(jié)點(diǎn)的alternate節(jié)點(diǎn)匠楚。
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
// node.childExpirationTime不需要更新巍膘,node.alternate.childExpirationTime也要嘗試更新
} else if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
// 找到了 FiberRoot 就跳出
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
// 沒找到則指針上溯,繼續(xù)下次while循環(huán)
node = node.return;
}
}
// 提醒當(dāng)前傳入的fiber沒有找到FiberRoot節(jié)點(diǎn)
if (root === null) {
if (__DEV__ && fiber.tag === ClassComponent) {
warnAboutUpdateOnUnmounted(fiber);
}
return null;
}
// 跟蹤應(yīng)用更新的相關(guān)代碼芋簿,精力和水平有限峡懈,汪洋大海不再深究。
if (enableSchedulerTracing) {
const interactions = __interactionsRef.current;
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
if (pendingInteractions != null) {
interactions.forEach(interaction => {
if (!pendingInteractions.has(interaction)) {
// Update the pending async work count for previously unscheduled interaction.
interaction.__count++;
}
pendingInteractions.add(interaction);
});
} else {
pendingInteractionMap.set(expirationTime, new Set(interactions));
// Update the pending async work count for the current interactions.
interactions.forEach(interaction => {
interaction.__count++;
});
}
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(
expirationTime,
root.interactionThreadID,
);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
return root;
}
3. requestWork
- 加入到 root 調(diào)度隊(duì)列
- 判斷是否批量更新
- 根據(jù) expirationTime 判斷調(diào)度類型
requestWork:
// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
// 關(guān)于批處理与斤,下一小節(jié)來探討
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, true);
}
return;
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
// 同步調(diào)用js代碼直到結(jié)束為止肪康,不會(huì)被打斷。
performSyncWork();
} else {
// 否則進(jìn)行異步調(diào)度撩穿,進(jìn)入到了獨(dú)立的scheduler包中, 即react的requestIdleCallback polyfill
// 等待有在deadline時(shí)間片內(nèi)才能得到執(zhí)行磷支,deadline到點(diǎn)后則控制權(quán)交給瀏覽器,等待下一個(gè)時(shí)間片食寡。
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
addRootToSchedule:
- 檢查 root 是否已參與調(diào)度雾狈,沒有則加入到 root 調(diào)度隊(duì)列,實(shí)際就是單向鏈表添加節(jié)點(diǎn)的操作抵皱。當(dāng)然絕大多數(shù)時(shí)候善榛,我們的應(yīng)用只有一個(gè) root。
- 如果 root 已經(jīng)參與調(diào)度呻畸,但可能需要提升優(yōu)先級(jí)移盆,使用 expirationTime 保存最高優(yōu)先級(jí)的任務(wù)。
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
// Add the root to the schedule.
// Check if this root is already part of the schedule.
if (root.nextScheduledRoot === null) {
// This root is not already scheduled. Add it.
root.expirationTime = expirationTime;
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
} else {
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
}
} else {
// This root is already scheduled, but its priority may have increased.
const remainingExpirationTime = root.expirationTime;
if (
remainingExpirationTime === NoWork ||
expirationTime < remainingExpirationTime
) {
// Update the priority.
root.expirationTime = expirationTime;
}
}
}
4. batchUpdates
顧名思義伤为,批量更新咒循,可以避免短期內(nèi)的多次渲染,攢為一次性更新绞愚。
在后面提供的 demo 中的 handleClick
中有三種方式調(diào)用 this.countNumber()
剑鞍。
**
第1種:
批量更新,會(huì)打印 0 0 0爽醋,然后按鈕文本顯示為3蚁署。每次 setState
雖然都會(huì)經(jīng)過 enqueueUpdate
(創(chuàng)建update 并加入隊(duì)列)-> scheduleWork
(尋找對應(yīng)的 FiberRoot 節(jié)點(diǎn))-> requestWork
(把 FiberRoot 加入到調(diào)度隊(duì)列),可惜上下文變量 isBatchingUpdates
在外部某個(gè)地方被標(biāo)記為了 true
,因此本次 setState
一路走來蚂四,尚未到達(dá)接下來的 performSyncWork
或者 scheduleCakkbackWithExpirationTime
就開始一路 return 出棧:
isBatchingUpdates
變量在早前的調(diào)用棧中(我們?yōu)?onClick 綁定的事件處理函數(shù)會(huì)被 react 包裹多層)光戈,被標(biāo)記為了 true
,然后 fn(a, b)
內(nèi)部經(jīng)過了3次 setState
系列操作遂赠,然后 finally 中 isBatchingUpdates
恢復(fù)為之前的 false久妆,此時(shí)執(zhí)行同步更新工作 performSyncWork
:
第2種:
在 handleClick
中使用 setTimeout
將 this.countNumber
包裹了一層 setTimeout(() => { this.countNumber()}, 0)
,同樣要調(diào)用 handleClick
也是先經(jīng)過 interactiveUpdates$1
上下文跷睦,也會(huì)執(zhí)行 setTimeout
筷弦,然后 fn(a, b)
就執(zhí)行完了,因?yàn)樽罱K是瀏覽器來調(diào)用 setTimeout
的回調(diào) 然后執(zhí)行里面的 this.countNumber
,而對于 interactiveUpdates$1
來說繼續(xù)把自己的 performSyncWork
執(zhí)行完烂琴,就算結(jié)束了爹殊。顯然不管 performSyncWork
做了什么同步更新,我們的 setState
目前為止都還沒得到執(zhí)行奸绷。然后等到 setTimeout
的回調(diào)函數(shù)等到空閑被執(zhí)行的時(shí)候梗夸,才會(huì)執(zhí)行 setState
,此時(shí)沒有了批量更新之上下文号醉,所以每個(gè) setState
都會(huì)單獨(dú)執(zhí)行一遍 requestWork
中的 performSyncWork
直到渲染結(jié)束反症,且不會(huì)被打斷,3次 setState
就會(huì)整個(gè)更新渲染 3 遍(這樣性能不好畔派,所以一般不會(huì)這樣寫 react)铅碍。
什么叫不會(huì)被打斷的同步更新渲染?看一下 demo 中的輸出线椰,每次都同步打印出了最新的 button dom 的 innerText
胞谈。
第3種:
已經(jīng)可以猜到,無非就是因?yàn)槭褂?setTimeout
而“錯(cuò)過了”第一次的批量更新上下文士嚎,那等到 setTimeout
的回調(diào)執(zhí)行的時(shí)候呜魄,專門再創(chuàng)建一個(gè)批量更新上下文即可:
**
所以,setState 是同步還是異步莱衩?
**
setState 方法本身是被同步調(diào)用爵嗅,但并不代表 react 的 state 就會(huì)被立馬同步地更新,而是要根據(jù)當(dāng)前執(zhí)行上下文來判斷笨蚁。
如果處于批量更新的情況下睹晒,state 不會(huì)立馬被更新,而是批量更新括细。
如果非批量更新的情況下伪很,那么就“有可能”是立馬同步更新的。為什么不是“一定”奋单?因?yàn)槿绻?React 開啟了 Concurrent Mode锉试,非批量更新會(huì)進(jìn)入之前介紹過的異步調(diào)度中(時(shí)間分片)。
批量更新演示 demo:
import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
export default class BatchedDemo extends React.Component {
state = {
number: 0,
}
handleClick = () => {
// 事件處理函數(shù)自帶batchedUpdates
// this.countNumber()
// setTimeout中沒有batchedUpdates
setTimeout(() => {
this.countNumber()
}, 0)
// 主動(dòng)batchedUpdates
// setTimeout(() => {
// batchedUpdates(() => this.countNumber())
// }, 0)
}
countNumber() {
const button = document.getElementById('myButton')
const num = this.state.number
this.setState({
number: num + 1,
})
console.log(this.state.number)
console.log(button.innerText)
this.setState({
number: num + 2,
})
console.log(this.state.number)
console.log(button.innerText)
this.setState({
number: num + 3,
})
console.log(this.state.number)
console.log(button.innerText)
}
render() {
return <button id="myButton" onClick={this.handleClick}>Num: {this.state.number}</button>
}
}
5. Scheduler 調(diào)度器
- 維護(hù)時(shí)間片
- 模擬 requestIdleCallback
- 調(diào)度列表和超時(shí)判斷
如果1秒30幀览濒,那么需要是平均的30幀呆盖,而不是前0.5秒1幀,后0.5秒29幀贷笛,這樣也會(huì)感覺卡頓的应又。
Scheduler 目的就是保證 React 執(zhí)行更新的時(shí)間,在瀏覽器的每一幀里不超過一定值乏苦。
不要過多占用瀏覽器用來渲染動(dòng)畫或者響應(yīng)用戶輸入的處理時(shí)間株扛。
**
繼續(xù)之前的源碼,requestWork 的最后,如果不是同步的更新任務(wù)洞就,那么就要參與 Scheduler 時(shí)間分片調(diào)度了:
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
scheduleCallbackWithExpirationTime:
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
// 如果已經(jīng)有在調(diào)度的任務(wù)盆繁,那么調(diào)度操作本身就是在循環(huán)遍歷任務(wù),等待即可奖磁。
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
// 因此改基,如果傳入的任務(wù)比已經(jīng)在調(diào)度的任務(wù)優(yōu)先級(jí)低繁疤,則返回
if (expirationTime > callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
// 但是咖为!如果傳入的任務(wù)優(yōu)先級(jí)更高,則要打斷已經(jīng)在調(diào)度的任務(wù)
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one.
} else {
startRequestCallbackTimer(); // 涉及到開發(fā)工具和polyfill稠腊,略過
}
// 如果是取消了老的調(diào)度任務(wù)躁染,或者是尚未有調(diào)度任務(wù),則接下來會(huì)安排調(diào)度
callbackExpirationTime = expirationTime;
// 計(jì)算出任務(wù)的timeout架忌,也就是距離此刻還有多久過期
const currentMs = now() - originalStartTimeMs; // originalStartTimeMs 代表react應(yīng)用最初被加載的那一刻
const expirationTimeMs = expirationTimeToMs(expirationTime);
const timeout = expirationTimeMs - currentMs;
// 類似于 setTimeout 返回的 ID吞彤,可以用來延期回調(diào)
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}
6. unstable_scheduleCallback
前面都還在 packages/react-reconciler/ReactFiberScheduler.js 中,下面就要跟著剛才的 **scheduleDeferredCallback **輾轉(zhuǎn)進(jìn)入到單獨(dú)的 packages/scheduler 包中:
- 根據(jù)不同優(yōu)先級(jí)等級(jí)計(jì)算不同的 callbackNode 上的過期時(shí)間叹放。
- 存儲(chǔ)以過期時(shí)間為優(yōu)先級(jí)的環(huán)形鏈表饰恕,用時(shí)可借助首節(jié)點(diǎn)
firstCallbackNode
可對鏈表進(jìn)行遍歷讀取。 -
firstCallbackNode
變了后要調(diào)用ensureHostCallbackIsScheduled
重新遍歷鏈表進(jìn)行調(diào)度井仰。
unstable_scheduleCallback:
function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
var newNode = {
callback,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
7. ensureHostCallbackIsScheduled
- 該方法名字就說明了目的是保證 callback 會(huì)被調(diào)度埋嵌,故若已經(jīng)有 callbackNode 在被調(diào)度,自會(huì)自動(dòng)循環(huán)俱恶。
- 從頭結(jié)點(diǎn)雹嗦,也就是最先過期的 callbackNode 開始請求調(diào)用,順表如果有已存在的調(diào)用要取消合是。這就是之前說過的參與調(diào)用的任務(wù)有兩種被打斷的可能:1. 時(shí)間片到點(diǎn)了了罪,2. 有更高優(yōu)先級(jí)的任務(wù)參與了調(diào)度
function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelHostCallback();
}
requestHostCallback(flushWork, expirationTime);
}
requestHostCallback = function(callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
// 超時(shí)了要立即安排調(diào)用
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
window.postMessage(messageKey, '*');
} else if (!isAnimationFrameScheduled) {
// 沒有超時(shí),就常規(guī)安排聪全,等待時(shí)間片
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
// 取消之前安排的任務(wù)回調(diào)泊藕,就是重置一些變量
cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
為了模擬 requestIdleCallback
API:
傳給 window.requestanimationframe
的回調(diào)函數(shù)會(huì)在瀏覽器下一次重繪之前執(zhí)行,也就是執(zhí)行該回調(diào)后瀏覽器下面會(huì)立即進(jìn)入重繪难礼。使用 window.postMessage
技巧將空閑工作推遲到重新繪制之后娃圆。
具體太過復(fù)雜,就大概聽個(gè)響吧鹤竭,若要深究則深究:
- animationTick
- idleTick
// 僅供示意
requestAnimationFrameWithTimeout(animationTick);
var animationTick = function(rafTime) {
requestAnimationFrameWithTimeout(animationTick);
}
window.addEventListener('message', idleTick, false);
window.postMessage(messageKey, '*');
react 這里還能統(tǒng)計(jì)判斷出平臺(tái)刷新頻率踊餐,來動(dòng)態(tài)減少 react 自身運(yùn)行所占用的時(shí)間片,支持的上限是 120hz 的刷新率臀稚,即每幀總共的時(shí)間不能低于 8ms吝岭。
此間如果一幀的時(shí)間在執(zhí)行 react js 之前就已經(jīng)被瀏覽器用完,那么對于非過期任務(wù),等待下次時(shí)間片窜管;而對于過期任務(wù)散劫,會(huì)強(qiáng)制執(zhí)行。
8. flushWork
ensureHostCallbackIsScheduled
中的 requestHostCallback(flushWork, expirationTime)
參與時(shí)間片調(diào)度:
flushWork:
- 即使當(dāng)前時(shí)間片已超時(shí)幕帆,也要把 callbackNode 鏈表中所有已經(jīng)過期的任務(wù)先強(qiáng)制執(zhí)行掉
- 若當(dāng)前幀還有時(shí)間片获搏,則常規(guī)處理任務(wù)
function flushWork(didTimeout) {
isExecutingCallback = true;
deadlineObject.didTimeout = didTimeout;
try {
// 把callbackNode鏈表中所有已經(jīng)過期的任務(wù)先強(qiáng)制執(zhí)行掉
if (didTimeout) {
// Flush all the expired callbacks without yielding.
while (firstCallbackNode !== null) {
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = getCurrentTime();
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime
);
continue;
}
break;
}
} else {
// 當(dāng)前幀還有時(shí)間片,則繼續(xù)處理任務(wù)
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
getFrameDeadline() - getCurrentTime() > 0
);
}
}
} finally {
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
flushFirstCallback
負(fù)責(zé)處理鏈表節(jié)點(diǎn)失乾,然后執(zhí)行 flushedNode.callback
常熙。
9. performWork
- 是否有 deadline 的區(qū)分
- 循環(huán)渲染 Root 的條件
- 超過時(shí)間片的處理
performSyncWork 不會(huì)傳 deadline。
沒有deadline時(shí)碱茁,會(huì)循環(huán)執(zhí)行 root 上的同步任務(wù)裸卫,或者任務(wù)過期了,也會(huì)立馬執(zhí)行任務(wù)纽竣。
performAsyncWork:
function performAsyncWork(dl) {
if (dl.didTimeout) { // 是否過期
if (firstScheduledRoot !== null) {
recomputeCurrentRendererTime();
let root: FiberRoot = firstScheduledRoot;
do {
didExpireAtExpirationTime(root, currentRendererTime);
// The root schedule is circular, so this is never null.
root = (root.nextScheduledRoot: any);
} while (root !== firstScheduledRoot);
}
}
performWork(NoWork, dl);
}
performSyncWork:
function performSyncWork() {
performWork(Sync, null);
}
performWork:
function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
deadline = dl;
// Keep working on roots until there's no more work, or until we reach
// the deadline.
findHighestPriorityRoot();
if (deadline !== null) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
if (enableUserTimingAPI) {
const didExpire = nextFlushedExpirationTime < currentRendererTime;
const timeout = expirationTimeToMs(nextFlushedExpirationTime);
stopRequestCallbackTimer(didExpire, timeout);
}
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
(minExpirationTime === NoWork ||
minExpirationTime >= nextFlushedExpirationTime) &&
(!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
) {
performWorkOnRoot(
nextFlushedRoot,
nextFlushedExpirationTime,
currentRendererTime >= nextFlushedExpirationTime,
);
findHighestPriorityRoot();
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
}
} else {
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
(minExpirationTime === NoWork ||
minExpirationTime >= nextFlushedExpirationTime)
) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
findHighestPriorityRoot();
}
}
// We're done flushing work. Either we ran out of time in this callback,
// or there's no more work left with sufficient priority.
// If we're inside a callback, set this to false since we just completed it.
if (deadline !== null) {
callbackExpirationTime = NoWork;
callbackID = null;
}
// If there's work left over, schedule a new callback.
if (nextFlushedExpirationTime !== NoWork) {
scheduleCallbackWithExpirationTime(
((nextFlushedRoot: any): FiberRoot),
nextFlushedExpirationTime,
);
}
// Clean-up.
deadline = null;
deadlineDidExpire = false;
finishRendering();
}
performWorkOnRoot:
- isRendering 標(biāo)記現(xiàn)在開始渲染了
- 判斷 finishedWork:是:調(diào)用 completeRoot 進(jìn)入下一章的 commit 階段墓贿;否:調(diào)用 renderRoot 遍歷 Fiber 樹。
function performWorkOnRoot(
root: FiberRoot,
expirationTime: ExpirationTime,
isExpired: boolean,
) {
isRendering = true;
// Check if this is async work or sync/expired work.
if (deadline === null || isExpired) {
// Flush work without yielding.
// TODO: Non-yieldy work does not necessarily imply expired work. A renderer
// may want to perform some work without yielding, but also without
// requiring the root to complete (by triggering placeholders).
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// If this root previously suspended, clear its existing timeout, since
// we're about to try rendering again.
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
const isYieldy = false;
renderRoot(root, isYieldy, isExpired);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Commit it.
completeRoot(root, finishedWork, expirationTime);
}
}
} else {
// Flush async work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// If this root previously suspended, clear its existing timeout, since
// we're about to try rendering again.
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
const isYieldy = true;
renderRoot(root, isYieldy, isExpired);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Check the deadline one more time
// before committing.
if (!shouldYield()) {
// Still time left. Commit the root.
completeRoot(root, finishedWork, expirationTime);
} else {
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
root.finishedWork = finishedWork;
}
}
}
}
isRendering = false;
}
10. renderRoot
- 調(diào)用 workLoop 進(jìn)行循環(huán)單元更新
- 捕獲錯(cuò)誤并進(jìn)行處理
- 走完流程之后善后
**renderRoot **流程:
- 遍歷 Fiber 樹的每個(gè)節(jié)點(diǎn)蜓氨。
根據(jù) Fiber 上的 updateQueue 是否有內(nèi)容聋袋,決定是否要更新那個(gè) Fiber 節(jié)點(diǎn),并且計(jì)算出新的 state穴吹,
對于異步任務(wù)幽勒,更新每個(gè) Fiber 節(jié)點(diǎn)時(shí)都要判斷時(shí)間片是否過期,如果一個(gè) Fiber 更新時(shí)出錯(cuò)刀荒,則其子節(jié)點(diǎn)就不用再更新了代嗤。最終整個(gè) Fiber 樹遍歷完之后,根據(jù)捕獲到的問題不同缠借,再進(jìn)行相應(yīng)處理干毅。
- createWorkInProgress:renderRoot 中,調(diào)用 createWorkInProgress 創(chuàng)建 “workInProgress” 樹泼返,在其上進(jìn)行更新操作硝逢。在 renderRoot 開始之后,所有的操作都在 “workInProgress” 樹上進(jìn)行绅喉,而非直接操作 “current” 樹渠鸽。(雙buff機(jī)制)
-
workLoop:開始更新一顆 Fiber 樹上的每個(gè)節(jié)點(diǎn),
isYieldy
指示是否可以中斷柴罐,對于 sync 任務(wù)和已經(jīng)超時(shí)的任務(wù)都是不可中斷的徽缚,于是 while 循環(huán)更新即可;對于可中斷的革屠,則每次 while 循環(huán)條件中還要判斷是否時(shí)間片到點(diǎn)需先退出凿试。
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
- performUnitOfWork:更新子樹:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (workInProgress.mode & ProfileMode) {
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
- beginWork:開始具體的節(jié)點(diǎn)更新排宰,下一章再說。
Root 節(jié)點(diǎn)具體怎么遍歷更新那婉,以及不同類型組件的更新板甘,將在下一篇探討。