本人系一個慣用Vue的菜雞,恰巧週末和大佬扯蛋,峯迴路轉談到了fiber,被大佬瘋狂鄙視...前端
大佬還和我吐槽瞭如今的忘了環境node
因而本菜開始了 React Fiber 相關的讀源碼過程。爲何看 Fiber?由於 Vue 沒有,Vue3 也沒有,可是卻被吹的很神奇。react
本菜於編寫時間於:2020/05/25
,參考的當日源碼版本 v16.13.1
git
首先必需要知道爲何會出現 Fibergithub
舊版本React同步更新:當React決定要加載或者更新組件樹時,會作不少事,好比調用各個組件的生命週期函數,計算和比對Virtual DOM,最後更新DOM樹。數組
舉個栗子:更新一個組件須要1毫秒,若是要更新1000個組件,那就會耗時1秒
,在這1秒
的更新過程當中,主線程都在專心運行更新操做。瀏覽器
而瀏覽器每間隔必定的時間從新繪製一下當前頁面。通常來講這個頻率是每秒60次。也就是說每16毫秒( 1 / 60 ≈ 0.0167 )瀏覽器會有一個週期性地重繪行爲,這每16毫秒咱們稱爲一幀。這一幀的時間裏面瀏覽器作些什麼事情呢:安全
若是這六個步驟中,任意一個步驟所佔用的時間過長,總時間超過 16ms 了以後,用戶也許就能看到卡頓。而上述栗子中組件同步更新耗時 1秒
,意味着差很少用戶卡頓了 1秒鐘!!!(差很少 - -!)bash
由於JavaScript單線程的特色,每一個同步任務不能耗時太長,否則就會讓程序不會對其餘輸入做出相應,React的更新過程就是犯了這個禁忌,而React Fiber就是要改變現狀。數據結構
解決同步更新的方案之一就是時間切片:把更新過程碎片化,把一個耗時長的任務分紅不少小片。執行非阻塞渲染,基於優先級應用更新以及在後臺預渲染內容。
Fiber 就是由 performUnitOfWork
(ps:後文詳細講述) 方法操控的 工做單元,做爲一種數據結構,用於表明某些worker,換句話說,就是一個work單元,經過Fiber的架構,提供了一種跟蹤,調度,暫停和停止工做的便捷方式。
Fiber的建立和使用過程:
workInProgressTree
用於計算更新(雙緩衝),能夠認爲是一顆表示當前工做進度的樹。還有一顆表示已渲染界面的舊樹,React就是一邊和舊樹比對,一邊構建WIP樹的。 alternate
指向舊樹的同等節點。PS:上文說的 workInProgress
屬於 beginWork
流程了,若是要寫下來差很少篇幅還會增長一倍,這就不詳細說明了...(主要是本人懶又菜...)
Fiber的體系結構分爲兩個主要階段:reconciliation
(協調)/render 和 commit
,
Reconciliation 階段在 Fiber重構後 和舊版本思路差異不大, 只不過不會再遞歸去比對、並且不會立刻提交變動。
涉及生命鉤子
reconciliation
特性:
完成 reconciliation 過程。這裏用的是 深度優先搜索(DFS)
,先處理子節點,再處理兄弟節點,直到循環完成。
涉及生命鉤子
render
和 commit
:不能暫停,會一直更新界面直到完成
對於UI來講須要考慮如下問題:
並非全部的state更新都須要當即顯示出來,好比:
因此,React 定義了一系列事件優先級
下面是優先級時間的源碼
[源碼文件](github.com/facebook/re…
var maxSigned31BitInt = 1073741823;
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;
複製代碼
當有更新任務來的時候,不會立刻去作 Diff 操做,而是先把當前的更新送入一個 Update Queue 中,而後交給 Scheduler
去處理,Scheduler 會根據當前主線程的使用狀況去處理此次 Update。
無論執行的過程怎樣拆分、以什麼順序執行,Fiber 都會保證狀態的一致性和視圖的一致性。
如何保證相同在必定時間內觸發的優先級同樣的任務到期時間相同? React 經過 ceiling
方法來實現的。。。本菜沒使用過 |
語法...
下面是處理到期時間的 ceiling
源碼
[源碼文件](github.com/facebook/re…
function ceiling(num, precision) {
return (((num / precision) | 0) + 1) * precision;
}
複製代碼
那麼爲何須要保證時間一致性?請看下文。
首先要找到調度入口地址 scheduleUpdateOnFiber
,
每個root都有一個惟一的調度任務,若是已經存在,咱們要確保到期時間與下一級別任務的相同(因此用上文提到的 ceiling
方法來控制到期時間)
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime, ) {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
// 調用markUpdateTimeFromFiberToRoot,更新 fiber 節點的 expirationTime
// ps 此時的fiber樹只有一個root fiber。
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return;
}
// TODO: computeExpirationForFiber also reads the priority. Pass the
// priority as an argument to that function and this one.
// 還只是TODO
// computeExpirationForFiber還會讀取優先級。
// 將優先級做爲參數傳遞給該函數和該函數。
const priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
if (
// Check if we're inside unbatchedUpdates
// 檢查是否在未批處理的更新內
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// Check if we're not already rendering
// 檢查是否還沒有渲染
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// Register pending interactions on the root to avoid losing traced interaction data.
// 在根上註冊待處理的交互,以免丟失跟蹤的交互數據。
schedulePendingInteractions(root, expirationTime);
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
// 推入調度任務隊列
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if (
(executionContext & DiscreteEventContext) !== NoContext &&
// Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
(priorityLevel === UserBlockingPriority ||
priorityLevel === ImmediatePriority)
) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
} else {
const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
if (
lastDiscreteTime === undefined ||
lastDiscreteTime > expirationTime
) {
rootsWithPendingDiscreteUpdates.set(root, expirationTime);
}
}
}
// Schedule other updates after in case the callback is sync.
ensureRootIsScheduled(root);
schedulePendingInteractions(root, expirationTime);
}
}
複製代碼
上面源碼主要作了如下幾件事
markUpdateTimeFromFiberToRoot
更新 Fiber 節點的 expirationTime
ensureRootIsScheduled
(更新重點)schedulePendingInteractions
實際上會調用 scheduleInteractions
scheduleInteractions
會利用FiberRoot的 pendingInteractionMap
屬性和不一樣的 expirationTime
,獲取每次schedule所需的update任務的集合,記錄它們的數量,並檢測這些任務是否會出錯。更新的重點在於 scheduleUpdateOnFiber
每一次更新都會調用 function ensureRootIsScheduled(root: FiberRoot)
下面是 ensureRootIsScheduled
的源碼
function ensureRootIsScheduled(root: FiberRoot) {
const lastExpiredTime = root.lastExpiredTime;
if (lastExpiredTime !== NoWork) {
// Special case: Expired work should flush synchronously.
root.callbackExpirationTime = Sync;
root.callbackPriority_old = ImmediatePriority;
root.callbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
return;
}
const expirationTime = getNextRootExpirationTimeToWorkOn(root);
const existingCallbackNode = root.callbackNode;
if (expirationTime === NoWork) {
// There's nothing to work on.
if (existingCallbackNode !== null) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
root.callbackPriority_old = NoPriority;
}
return;
}
// TODO: If this is an update, we already read the current time. Pass the
// time as an argument.
const currentTime = requestCurrentTimeForUpdate();
const priorityLevel = inferPriorityFromExpirationTime(
currentTime,
expirationTime,
);
// If there's an existing render task, confirm it has the correct priority and
// expiration time. Otherwise, we'll cancel it and schedule a new one.
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority_old;
const existingCallbackExpirationTime = root.callbackExpirationTime;
if (
// Callback must have the exact same expiration time.
existingCallbackExpirationTime === expirationTime &&
// Callback must have greater or equal priority.
existingCallbackPriority >= priorityLevel
) {
// Existing callback is sufficient.
return;
}
// Need to schedule a new task.
// TODO: Instead of scheduling a new task, we should be able to change the
// priority of the existing one.
cancelCallback(existingCallbackNode);
}
root.callbackExpirationTime = expirationTime;
root.callbackPriority_old = priorityLevel;
let callbackNode;
if (expirationTime === Sync) {
// Sync React callbacks are scheduled on a special internal queue
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
callbackNode = scheduleCallback(
priorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
} else {
callbackNode = scheduleCallback(
priorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
// Compute a task timeout based on the expiration time. This also affects
// ordering because tasks are processed in timeout order.
{timeout: expirationTimeToMs(expirationTime) - now()},
);
}
root.callbackNode = callbackNode;
}
複製代碼
上面源碼 ensureRootIsScheduled
主要是根據同步/異步狀態作不一樣的 push 功能。
同步調度 function scheduleSyncCallback(callback: SchedulerCallback)
:
syncQueue.push(callback)
)Scheduler_scheduleCallback
)performSyncWorkOnRoot
做爲 SchedulerCallback
下面是 scheduleSyncCallback
源碼內容
export function scheduleSyncCallback(callback: SchedulerCallback) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
if (syncQueue === null) {
syncQueue = [callback];
// Flush the queue in the next tick, at the earliest.
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
syncQueue.push(callback);
}
return fakeCallbackNode;
}
複製代碼
異步調度,異步的任務調度很簡單,直接將異步任務推入調度隊列(Scheduler_scheduleCallback
),會將 performConcurrentWorkOnRoot
做爲 SchedulerCallback
export function scheduleCallback( reactPriorityLevel: ReactPriorityLevel, callback: SchedulerCallback, options: SchedulerCallbackOptions | void | null, ) {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
複製代碼
無論同步調度仍是異步調度,都會通過 Scheduler_scheduleCallback
也就是調度的核心方法 function unstable_scheduleCallback(priorityLevel, callback, options)
,它們會有各自的 SchedulerCallback
小提示:因爲下面不少代碼中會使用 peek
,先插一段 peek
實現,其實就是返回數組中的第一個 或者 null
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
複製代碼
下面是 Scheduler_scheduleCallback
相關源碼
[源碼文件](github.com/facebook/re…
// 將一個任務推入任務調度隊列
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
var startTime;
var timeout;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
timeout =
typeof options.timeout === 'number'
? options.timeout
: timeoutForPriorityLevel(priorityLevel);
} else {
// 針對不一樣的優先級算出不一樣的過時時間
timeout = timeoutForPriorityLevel(priorityLevel);
startTime = currentTime;
}
// 定義新的過時時間
var expirationTime = startTime + timeout;
// 定義一個新的任務
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
// 將超時的任務推入超時隊列
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
// 當全部任務都延遲時,並且該任務是最先的任務
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
// 將新的任務推入任務隊列
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
// 執行回調方法,若是已經再工做須要等待一次回調的完成
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
(flushWork);
}
}
return newTask;
}
複製代碼
小提示: markTaskStart
主要起到記錄的功能,對應的是 markTaskCompleted
export function markTaskStart( task: { id: number, priorityLevel: PriorityLevel, ... }, ms: number, ) {
if (enableProfiling) {
profilingState[QUEUE_SIZE]++;
if (eventLog !== null) {
// performance.now returns a float, representing milliseconds. When the
// event is logged, it's coerced to an int. Convert to microseconds to
// maintain extra degrees of precision.
logEvent([TaskStartEvent, ms * 1000, task.id, task.priorityLevel]);
}
}
}
export function markTaskCompleted( task: { id: number, priorityLevel: PriorityLevel, ... }, ms: number, ) {
if (enableProfiling) {
profilingState[PRIORITY] = NoPriority;
profilingState[CURRENT_TASK_ID] = 0;
profilingState[QUEUE_SIZE]--;
if (eventLog !== null) {
logEvent([TaskCompleteEvent, ms * 1000, task.id]);
}
}
}
複製代碼
unstable_scheduleCallback
主要作了幾件事
options.delay
和 options.timeout
加上 timeoutForPriorityLevel()
來得到 newTask
的 expirationTime
cancelHostTimeout
requestHostTimeout
補上 cancelHostTimeout
源碼
cancelHostTimeout = function() {
clearTimeout(_timeoutID);
};
複製代碼
再補上 requestHostTimeout
源碼
requestHostTimeout = function(cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
複製代碼
而後 requestHostTimeout
的 cb
也就是 handleTimeout
是啥呢?
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
複製代碼
上面這個方法很重要,它主要作了下面幾件事
advanceTimers
檢查再也不延遲的任務,並將其添加到隊列中。下面是 advanceTimers
源碼
function advanceTimers(currentTime) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// Timer was cancelled.
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// Timer fired. Transfer to the task queue.
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}
複製代碼
requestHostCallback
經過 MessageChannel
的異步方法來開啓任務調度 performWorkUntilDeadline
requestHostCallback
這個方法特別重要
// 經過onmessage 調用 performWorkUntilDeadline 方法
channel.port1.onmessage = performWorkUntilDeadline;
// postMessage
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
複製代碼
而後是同文件下的 performWorkUntilDeadline
,調用了 scheduledHostCallback
, 也就是以前傳入的 flushWork
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
複製代碼
flushWork
主要的做用是調用 workLoop
去循環執行全部的任務
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod codepath.
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
複製代碼
workLoop
和 flushWork
在一個文件中,做用是從調度任務隊列中取出優先級最高的任務,而後去執行。
還記得上文講的 SchedulerCallback
嗎?
performSyncWorkOnRoot
performConcurrentWorkOnRoot
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
複製代碼
最終都會經過 performUnitOfWork
操做。
這個方法只不過異步的方法是能夠打斷的,咱們每次調用都要查看是否超時。
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, renderExpirationTime);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, renderExpirationTime);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
複製代碼
上面的 startProfilerTimer
和 stopProfilerTimerIfRunningAndRecordDelta
其實就是記錄 fiber 的工做時長。
function startProfilerTimer(fiber: Fiber): void {
if (!enableProfilerTimer) {
return;
}
profilerStartTime = now();
if (((fiber.actualStartTime: any): number) < 0) {
fiber.actualStartTime = now();
}
}
function stopProfilerTimerIfRunningAndRecordDelta( fiber: Fiber, overrideBaseTime: boolean, ): void {
if (!enableProfilerTimer) {
return;
}
if (profilerStartTime >= 0) {
const elapsedTime = now() - profilerStartTime;
fiber.actualDuration += elapsedTime;
if (overrideBaseTime) {
fiber.selfBaseDuration = elapsedTime;
}
profilerStartTime = -1;
}
}
複製代碼
最後,就到了 beginWork
流程了 - -。裏面有什麼呢? workInProgress
還有一大堆的 switch case
。
想看 beginWork
源碼的能夠自行嘗試 beginWork相關源碼文件
最後是總結部分,該不應寫這個想了好久,每一個讀者在不一樣時間不一樣心境下看源碼的感悟應該是不同的(固然本身回顧的時候也是讀者)。每次看應該都有每一個時期的總結。
可是若是不寫總結,這篇解析又感受枯燥無味,且沒有結果。因此簡單略過一下(確定是原創啦,別的地方沒有的)
expirationTime
獲得過時時間settimeout
+ postMessage
實現的clearTimeout
使用鏈表結構只是一個結果,而不是目的,React 開發者一開始的目的是衝着模擬調用棧去的
調用棧最常常被用於存放子程序的返回地址。在調用任何子程序時,主程序都必須暫存子程序運行完畢後應該返回到的地址。所以,若是被調用的子程序還要調用其餘的子程序,其自身的返回地址就必須存入調用棧,在其自身運行完畢後再行取回。除了返回地址,還會保存本地變量、函數參數、環境傳遞。
所以 Fiber 對象被設計成一個鏈表結構,經過如下主要屬性組成一個鏈表
type
類型return
存儲當前節點的父節點child
存儲第一個子節點sibling
存儲右邊第一個的兄弟節點alternate
舊樹的同等節點咱們在遍歷 dom 樹 diff 的時候,即便中斷了,咱們只須要記住中斷時候的那麼一個節點,就能夠在下個時間片恢復繼續遍歷並 diff。這就是 fiber 數據結構選用鏈表的一大好處。
瀏覽器個週期執行的事件
1. 宏任務
2. 微任務
4. requestAnimationFrame
5. IntersectionObserver
6. 更新界面
7. requestIdleCallback
8. 下一幀
複製代碼
根據官方描述:
window.requestIdleCallback()
方法將在瀏覽器的空閒時段內調用的函數排隊。這使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。函數通常會按先進先調用的順序執行,然而,若是回調函數指定了執行超時時間timeout
,則有可能爲了在超時前執行函數而打亂執行順序。 你能夠在空閒回調函數中調用requestIdleCallback()
,以便在下一次經過事件循環以前調度另外一個回調。
看似完美契合時間切片的思想,因此起初 React 的時間分片渲染就想要用到這個 API,不過目前瀏覽器支持的不給力,並且 requestIdleCallback
有點過於嚴格,而且執行頻率不足以實現流暢的UI呈現。
並且咱們但願經過Fiber 架構,讓 reconcilation
過程變成可被中斷。'適時'地讓出 CPU 執行權。所以React團隊不得不實現本身的版本。
實際上 Fiber 的思想和協程的概念是契合的。舉個栗子:
普通函數: (沒法被中斷和恢復)
const tasks = []
function run() {
let task
while (task = tasks.shift()) {
execute(task)
}
}
複製代碼
若是使用 Generator
語法:
const tasks = []
function * run() {
let task
while (task = tasks.shift()) {
// 判斷是否有高優先級事件須要處理, 有的話讓出控制權
if (hasHighPriorityEvent()) {
yield
}
// 處理完高優先級事件後,恢復函數調用棧,繼續執行...
execute(task)
}
}
複製代碼
可是 React 嘗試過用 Generator 實現,後來發現很麻煩,就放棄了。
主要是2個緣由:
Generator
必須將每一個函數都包裝在 Generator 堆棧中。這不只增長了不少語法開銷,並且還增長了現有實現中的運行時開銷。雖然有勝於無,可是性能問題仍然存在。是否能夠經過 Web Worker
來建立多線程環境來實現時間切片呢?
React 團隊也曾經考慮過,嘗試提出共享的不可變持久數據結構,嘗試了自定義 VM 調整等,可是 JavaScript
該語言不適用於此。
由於可變的共享運行時(例如原型),生態系統尚未作好準備,由於你必須跨工做人員重複代碼加載和模塊初始化。若是垃圾回收器必須是線程安全的,則它們的效率不如當前高效,而且VM實現者彷佛不肯意承擔持久數據結構的實現成本。共享的可變類型數組彷佛正在發展,可是在當今的生態系統中,要求全部數據經過此層彷佛是不可行的。代碼庫的不一樣部分之間的人爲邊界也沒法很好地工做,而且會帶來沒必要要的摩擦。即便那樣,你仍然有不少JS代碼(例如實用程序庫)必須在工做人員之間複製。這會致使啓動時間和內存開銷變慢。所以,是的,在咱們能夠定位諸如Web Assembly之類的東西以前,線程多是不可能的。
你沒法安全地停止後臺線程。停止和重啓線程並非很便宜。在許多語言中,它也不安全,由於你可能處於一些懶惰的初始化工做之中。即便它被有效地中斷了,你也必須繼續在它上面花費CPU週期。
另外一個限制是,因爲沒法當即停止線程,所以沒法肯定兩個線程是否同時處理同一組件。這致使了一些限制,例如沒法支持有狀態的類實例(如React.Component)。線程不能只記住你在一個線程中完成的部分工做並在另外一個線程中重複使用。
ps: 本菜不會用 React,第一次讀 React 源碼,對源碼有誤讀請指正
全棧
或 Vue
有好禮相送哦