React Fiber原始碼分析 第三篇(非同步狀態)
先附上流程圖~
呼叫 setState 時, 會呼叫 classComponentUpdater 的 enqueueSetState 方法, 同時將新的 state 作為 payload 引數傳進
enqueueSetState 會先呼叫 requestCurrentTime 獲取一個 currentTime ,
function requestCurrentTime() { // 維護兩個時間 一個renderingTime 一個currentSechedulerTime //rederingTime 可以隨時更新currentSechedulerTime只有在沒有新任務的時候才更新 if (isRendering) { return currentSchedulerTime; } findHighestPriorityRoot(); if (nextFlushedExpirationTime === NoWork || nextFlushedExpirationTime === Never) { recomputeCurrentRendererTime(); currentSchedulerTime = currentRendererTime; return currentSchedulerTime; } return currentSheculerTime
通過獲取到的 currentTime , 呼叫 computeExpirationForFiber ,計算該fiber的優先順序,
if (fiber.mode & AsyncMode) { if (isBatchingInteractiveUpdates) { // This is an interactive update expirationTime = computeInteractiveExpiration(currentTime); } else { // This is an async update expirationTime = computeAsyncExpiration(currentTime); } ... }
這個函式其他點比較簡單, 裡面主要有下面 這個判斷要說明一下, 如果是屬於非同步更新的話,會根據是 互動引起的更新 還是其他更新 來呼叫不同的函式 computeInteractiveExpiration 和 computeAsyncExpiration ,
可以看到這兩個函式最後返回的都是 computeExpirationBucket 函式的結果, 只是入參不同, computeInteractiveExpiration 的引數是 500, 100, computeAsyncExpiration 的引數是 5000, 250 , 然後看 computeExpirationBucket 函式可以看到, 第二個引數 (500和5000) 越大,則返回的 expirationTime 越大, 也就是說 computeInteractiveExpiration 的更新優先順序高於 computeAsyncExpiration , 則互動的優先順序高於其他
獲得優先順序後則和同步更新一樣, 建立 update 並放進佇列, 然後呼叫 sheuduleWork
var classComponentUpdater = { isMounted: isMounted, enqueueSetState: function (inst, payload, callback) { var fiber = get(inst); // 獲得優先順序 var currentTime = requestCurrentTime(); var expirationTime = computeExpirationForFiber(currentTime, fiber); // 建立更新 var update = createUpdate(expirationTime); update.payload = payload; if (callback !== undefined && callback !== null) { update.callback = callback; } enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); },
接下來的步驟和同步一樣, 直到同步呼叫的是 performSyncWork 函式, 而非同步呼叫的是 scheduleCallbackWithExpirationTime 函式
scheduleCallbackWithExpirationTime函式首先判斷是否存在 callback 正在進行中, 判斷現有 expirationTime 和其優先順序,若優先順序比較低則直接返回, 否則設定現在的 fiber 任務為新的 callback ,並把原來的回撥從列表中移除
function scheduleCallbackWithExpirationTime(root, expirationTime) { if (callbackExpirationTime !== NoWork) { //判斷優先順序 if (expirationTime > callbackExpirationTime) { // Existing callback has sufficient timeout. Exit. return; } else { if (callbackID !== null) { // 取消, 從回撥列表中刪除 schedule.unstable_cancelScheduledWork(callbackID); } } // The request callback timer is already running. Don't start a new one. } // 設定新的callback和callbackExiporationTime callbackExpirationTime = expirationTime; var currentMs = schedule.unstable_now() - originalStartTimeMs; var expirationTimeMs = expirationTimeToMs(expirationTime); // 計算是否超時 var timeout = expirationTimeMs - currentMs; callbackID = schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout }); }
接下來呼叫 schedule.unstable_scheduleWork(performAsyncWork, { timeout: timeout }) 函式, 並 生成一個節點, 儲存回撥函式和超時時間,插入到回撥列表, 並根據超時排序, 呼叫 ensureHostCallBackIsScheduled 函式,最後返回該節點
function unstable_scheduleWork(callback, options) { var currentTime = exports.unstable_now(); var timesOutAt; // 獲取超時時間 if (options !== undefined && options !== null && options.timeout !== null && options.timeout !== undefined) { // Check for an explicit timeout timesOutAt = currentTime + options.timeout; } else { // Compute an absolute timeout using the default constant. timesOutAt = currentTime + DEFERRED_TIMEOUT; } // 生成一個節點, 儲存回撥函式和超時時間 var newNode = { callback: callback, timesOutAt: timesOutAt, next: null, previous: null }; // 插入到回撥列表, 並根據超時排序, 最後返回該節點 if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = newNode.next = newNode.previous = newNode; ensureHostCallbackIsScheduled(firstCallbackNode); } else { ...var previous = next.previous; previous.next = next.previous = newNode; newNode.next = next; newNode.previous = previous; } return newNode; }
ensureHostCallBackIsScheduled函式如名, 相對比較簡單
function ensureHostCallbackIsScheduled() { if (isPerformingWork) { // Don't schedule work yet; wait until the next time we yield. return; } // Schedule the host callback using the earliest timeout in the list. var timesOutAt = firstCallbackNode.timesOutAt; if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; } else { // Cancel the existing host callback. cancelCallback(); } requestCallback(flushWork, timesOutAt); }
往下看 requestCallback , 這裡說的 如果已經在執行任務的話, 就必須有一個錯誤被丟擲(丟擲的錯誤是啥??),同時不要等待下一幀, 儘快開始新事件
如果當前沒有排程幀回撥函式,我們需要進行一個排程幀回撥函式, 並設定 isAnimationFrameScheduled 為 true ,
接著執行
requestAnimationFrameWithTimeout ;函式requestCallback = function (callback, absoluteTimeout) { scheduledCallback = callback; timeoutTime = absoluteTimeout; if (isPerformingIdleWork) { // 如果已經在執行任務的話, 就必須有一個錯誤被丟擲(丟擲的錯誤是啥??),同時不要等待下一幀, 儘快開始新事件 window.postMessage(messageKey, '*'); } else if (!isAnimationFrameScheduled) { isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } };
requestAnimationFrameWithTimeout函式就是執行一個非同步操作, 執行完畢後, 假設此時又有N個回撥任務進入, 同時原來的回撥還沒有進行, 則回到 scheduleCallbackWithExpirationTime 函式上,
分為兩個分支: 1. 假設優先順序低於目前的回撥任務, 則直接返回(已經把 root 加到 root 佇列中)
2. 優先順序高於目前的回撥任務, 將目前的回撥任務從列表中移除, 並將 callBackID 設為傳入的回撥, 接下來的路線與上面一致, 假設該傳入的回撥超時最早, 則會進入到 cancelCallback 函式,重 置各變數, 並進入到 requestCallback 函式, 此時除了賦值操作, 沒有其他動作
到了這時候, 已經把新的回撥替換正在進行的回撥到回撥列表。
函式正常執行, 呼叫 callback, 即 animationTick 函式
cancelCallback = function () { scheduledCallback = null; isIdleScheduled = false; timeoutTime = -1; };
var ANIMATION_FRAME_TIMEOUT = 100; var rAFID; var rAFTimeoutID; var requestAnimationFrameWithTimeout = function (callback) { // schedule rAF and also a setTimeout rAFID = localRequestAnimationFrame(function (timestamp) { // cancel the setTimeout localClearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = localSetTimeout(function () { // cancel the requestAnimationFrame localCancelAnimationFrame(rAFID); callback(exports.unstable_now()); }, ANIMATION_FRAME_TIMEOUT); };
animationTick一個是把 isAnimationFrameScheduled 狀態設為 false , 即不在排程幀回撥的狀態, 同時計算幀到期時間 frameDeadline , 判斷是否在幀回撥的狀態, 否的話呼叫 window.postMessage ,並設定 isIdleScheduled 狀態為 true
假設此時, 有N個回撥進入, 分為兩個情況: 1.假設優先順序低於目前的回撥任務, 則直接返回(已經把 root 加到 root 佇列中)
2.優先順序高於目前的回撥任務, 將目前的回撥任務從列表中移除, 並將 callBackID 設為傳入的回撥, 接下來的路線與上面一致,一直到 animationTick 函式,因為 postMessage 比 setTImeout 更快執行,所以此時 isIdleScheduled 為 false ,和之前一樣正常執行。
var animationTick = function (rafTime) { isAnimationFrameScheduled = false; ... ... // 每幀到期時間為33ms frameDeadline = rafTime + activeFrameTime; if (!isIdleScheduled) { isIdleScheduled = true; window.postMessage(messageKey, '*'); } };
postMessage會執行 idleTick , 首先把 isIdleScheduled\didTimeout 置為 false ,
先判斷幀到期時間和超時時間是否小於當前時間, 如果是的話, 則置 didTimeout 為 true ,
如果幀到期, 但超時時間小於當前時間, 則置 isAnimationFrameScheduled 為 false , 並呼叫 requestAnimationFrameWithTimeout , 即進入下一幀
如果幀未到期, 則呼叫 callbak 函式, 並把 isPerformingIdleWork 置為 true
idleTick會先執行 callback , 完成後才將 isPerformingIdleWork 置為 false , 執行 callback 的時候會傳入 didTimeout 作為引數, callback 為 flushWork
var idleTick = function (event) { ... isIdleScheduled = false; var currentTime = exports.unstable_now(); var didTimeout = false; if (frameDeadline - currentTime <= 0) { // 幀過期 if (timeoutTime !== -1 && timeoutTime <= currentTime) { // 回撥超時 didTimeout = true; } else { // No timeout. if (!isAnimationFrameScheduled) { // 到下一幀繼續任務 isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } // Exit without invoking the callback. return; } } timeoutTime = -1; var callback = scheduledCallback; scheduledCallback = null; if (callback !== null) { isPerformingIdleWork = true; try { callback(didTimeout); } finally { isPerformingIdleWork = false; } } };
flushwork首先把 isPerformingWork 置為 true , 然後把 didTimeout 賦值給 deallinObject 物件, 接下來進行判斷
如果已經過了幀的結束期, 則判斷連結串列中有哪個節點已超時, 並迴圈呼叫 flushFirstCallback 函式解決超時節點,
如果還沒有過幀的結束期, 則呼叫 flushFirstCallback 函式處理連結串列中的第一個節點, 迴圈處理一直到該幀結束
最後, flushwork 函式會將 isPerformingWork 置為 false , 並判斷是否還有任務 有則執行 ensureHostCallbackIsScheduled 函式
function flushWork(didTimeout) { isPerformingWork = true; deadlineObject.didTimeout = didTimeout; try { if (didTimeout) { while (firstCallbackNode !== null) { var currentTime = exports.unstable_now(); if (firstCallbackNode.timesOutAt <= currentTime) { do { flushFirstCallback(); } while (firstCallbackNode !== null && firstCallbackNode.timesOutAt <= currentTime); continue; } break; } } else { // Keep flushing callbacks until we run out of time in the frame. if (firstCallbackNode !== null) { do { flushFirstCallback(); } while (firstCallbackNode !== null && getFrameDeadline() - exports.unstable_now() > 0); } } } finally { isPerformingWork = false; if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(firstCallbackNode); } else { isHostCallbackScheduled = false; } } }
繼續往下看, 則是 flushFirstCallback 函式,先把該節點從連結串列中清掉, 然後呼叫 callback 函式, 並帶入 deadlineObject 作為引數
function flushFirstCallback(node) { var flushedNode = firstCallbackNode; //從連結串列中清理掉該節點, 這樣哪怕出錯了, 也能保留原連結串列狀態 var next = firstCallbackNode.next; if (firstCallbackNode === next) { // This is the last callback in the list. firstCallbackNode = null; next = null; } else { var previous = firstCallbackNode.previous; firstCallbackNode = previous.next = next; next.previous = previous; } flushedNode.next = flushedNode.previous = null; // Now it's safe to call the callback. var callback = flushedNode.callback; callback(deadlineObject); }
接下來的就是 performAsyncWork 函式,如果 didTimeout 為 true , 則表明至少有一個更新已過期, 迭代所有 root 任務, 把已過期的 root 的 nextExpirationTimeToWorkOn 重置為當前時間 currentTime .
然後呼叫 performWork(Nowork, dl) 函式
function performAsyncWork(dl) { if (dl.didTimeout) { // 重新整理所有root的nextEpirationTimeToWorkOn if (firstScheduledRoot !== null) { recomputeCurrentRendererTime(); var root = firstScheduledRoot; do { didExpireAtExpirationTime(root, currentRendererTime); // The root schedule is circular, so this is never null. root = root.nextScheduledRoot; } while (root !== firstScheduledRoot); } } performWork(NoWork, dl); }
performWork函式在之前已經分析過了, 這裡主要看存在 deadline 時的操作, 在幀未到期 或者 當前渲染時間大於等於 nextFlushedExpirationTime 時才執行 performWorkOnRoot , 並將 currentRendererTime >= nextFlushedExpirationTime作為 第三個引數傳入, 一直迴圈處理任務,
最後清除 callbackExpirationTime, callBackId, 同時, 如果還有任務的話, 則繼續呼叫 scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime) ;函式進入到回撥
function performWork(minExpirationTime, dl) { 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;while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || minExpirationTime >= nextFlushedExpirationTime) && (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)) { performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, currentRendererTime >= nextFlushedExpirationTime); findHighestPriorityRoot(); recomputeCurrentRendererTime(); currentSchedulerTime = currentRendererTime; } } if (deadline !== null) { callbackExpirationTime = NoWork; callbackID = null; } // If there's work left over, schedule a new callback. if (nextFlushedExpirationTime !== NoWork) { scheduleCallbackWithExpirationTime(nextFlushedRoot, nextFlushedExpirationTime); } // Clean-up. deadline = null; deadlineDidExpire = false; finishRendering(); }
接下來看非同步狀態下的 performWorkOnRoot 函式。基本操作和同步一樣, 在進入到 renderRoot(root, _isYieldy, isExpired); 函式時, 會根據是否已超時將 isYieldy 置為 true 或者 false , 非同步狀態下未超時為 false ,
renderRoot和同步一樣, 最後執行 workLoop(isYieldy)
workLoop在未過期的情況下, 會執行 shouldYield() 函式來判斷是否執行 nextUnitOfWork , 和同步一樣, 這裡只需要關注 shouldYied 函式
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); } } }
shouldYield函式, 如果 deadlineDidExpire 為 true , 即幀已到期, 直接返回 true ,
如果 deadline 不存在, 並且幀未到期, 則返回 false , 可以執行單元
否則將 deadlineDidExpire 置為 true
function shouldYield() { if (deadlineDidExpire) { return true; } if (deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork) { // Disregard deadline.didTimeout. Only expired work should be flushed // during a timeout. This path is only hit for non-expired work. return false; } deadlineDidExpire = true; return true; }
完結~撒花