前言:
在React源碼解析之scheduleWork(下)中,咱們講到了unstable_scheduleCallback
,其中在「按計劃插入調度任務」後,會調用requestHostCallback()
方法:javascript
function unstable_scheduleCallback(priorityLevel, callback, options) {
xxx
//若是開始調度的時間已經錯過了
if (startTime > currentTime) {
xxx
}
//沒有延期的話,則按計劃插入task
else {
xxx
//更新調度執行的標誌
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
/*--------------------這裏------------------*/
//執行調度 callback
requestHostCallback(flushWork);
/*-------------------------------------------*/
}
}
}
複製代碼
本文的目的就是講解requestHostCallback()
的源碼邏輯及其做用。java
1、requestHostCallback()
做用:
執行 React 的調度任務react
源碼:nginx
//在每一幀內執行調度任務(callback)
requestHostCallback = function(callback) {
if (scheduledHostCallback === null) {
//firstCallbackNode 傳進來的 callback
scheduledHostCallback = callback;
//若是 react 在幀裏面還未超時(即多佔用了瀏覽器的時間)
//還未開始調度
if (!isAnimationFrameScheduled) {
// If rAF didn't already schedule one, we need to schedule a frame.
// TODO: If this rAF doesn't materialize because the browser throttles,
// we might want to still have setTimeout trigger rIC as a backup to
// ensure that we keep performing work.
//開始調度
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
}
};
複製代碼
解析:
初始化調度任務scheduledHostCallback
、調度標識isAnimationFrameScheduled
,並執行requestAnimationFrameWithTimeout()
方法,animationTick()
函數後面解析git
2、requestAnimationFrameWithTimeout()
做用:
在每一幀內執行調度任務github
源碼:web
// This initialization code may run even on server environments if a component
// just imports ReactDOM (e.g. for findDOMNode). Some environments might not
// have setTimeout or clearTimeout. However, we always expect them to be defined
// on the client. https://github.com/facebook/react/pull/13088
const localSetTimeout =
typeof setTimeout === 'function' ? setTimeout : undefined;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : undefined;
// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
const localRequestAnimationFrame =
typeof requestAnimationFrame === 'function'
? requestAnimationFrame
: undefined;
const localCancelAnimationFrame =
typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
//最晚執行時間爲 100ms
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
//防止localRequestAnimationFrame長時間(100ms)內沒有調用,
//強制執行 callback 函數
const requestAnimationFrameWithTimeout = function(callback) {
// schedule rAF and also a setTimeout
/*若是 A 執行了,則取消 B*/
//就是window.requestAnimationFrame API
//若是屏幕刷新率是 30Hz,即一幀是 33ms 的話,那麼就是每 33ms 執行一次
//timestamp表示requestAnimationFrame() 開始去執行回調函數的時刻,是requestAnimationFrame自帶的參數
rAFID = localRequestAnimationFrame(function(timestamp) {
// cancel the setTimeout
//已經比 B 先執行了,就取消 B 的執行
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
//若是超過 100ms 仍未執行的話
/*若是 B 執行了,則取消 A*/
rAFTimeoutID = localSetTimeout(function() {
// cancel the requestAnimationFrame
//取消 localRequestAnimationFrame
localCancelAnimationFrame(rAFID);
//直接調用回調函數
callback(getCurrentTime());
//100ms
}, ANIMATION_FRAME_TIMEOUT);
};
複製代碼
解析:
(1)localRequestAnimationFrame
即window.requestAnimationFrame
,
做用是和瀏覽器刷新頻率保持同步的時候,執行內部的 callback,
具體請看:window.requestAnimationFrame跨域
(2)requestAnimationFrameWithTimeout
內部是兩個function
:
① rAFID
就是執行window.requestAnimationFrame
方法,若是先執行,就清除 ② 的rAFTimeoutID
瀏覽器
人眼能接受不卡頓的頻率是 30Hz,即每秒 30 幀,1 幀是 33ms,這也是 React 默認瀏覽器的刷新頻率(下文會解釋)app
也就是說,若是 ① rAFID 先執行的話,即會隨着瀏覽器刷新頻率執行,而且會阻止 ② rAFTimeoutID
的執行。
② rAFTimeoutIDrAFTimeoutID
的做用更像是一個保底措施,若是 React 在進入調度流程,而且有調度隊列存在,可是 100ms 仍未執行調度任務的話,則強制執行調度任務,而且阻止 ① rAFID
的執行。
也就是說 ① rAFID 和 ② rAFTimeoutID 是「競爭關係」,誰先執行,就阻止對方執行。
執行requestAnimationFrameWithTimeout
方法時,帶的參數是animationTick
:
requestAnimationFrameWithTimeout(animationTick);
複製代碼
接下來說下animationTick
方法
3、animationTick()
做用:
計算每一幀中 react 進行調度任務的時長,並執行該 callback
源碼:
let frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
let previousFrameTime = 33;
//保持瀏覽器每秒 30 幀的狀況下,每一幀爲 33ms
let activeFrameTime = 33;
//計算每一幀中 react 進行調度任務的時長,並執行該 callback
const animationTick = function(rafTime) {
//若是不爲 null 的話,當即請求下一幀重複作這件事
//這麼作的緣由是:調度隊列有多個 callback,
// 不能保證在一個 callback 完成後,恰好能在下一幀繼續執行下一個 callback,
//因此在當前 callback 存在的同時,執行下一幀的 callback
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it's fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
//沒有 callback 要被調度,退出
isAnimationFrameScheduled = false;
return;
}
//用來計算下一幀有多少時間是留給react 去執行調度的
//rafTime:requestAnimationFrame執行的時間
//frameDeadline:0 ,每一幀執行後,超出的時間
//activeFrameTime:33,每一幀的執行事件
let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
//若是調度執行時間沒有超過一幀時間
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime &&
!fpsLocked
) {
//React 不支持每一幀比 8ms 還要短,即 120 幀
//小於 8ms 的話,強制至少有 8ms 來執行調度
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that's an indication that we
// actually have a higher frame rate than what we're currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we're
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.
//哪一個長選哪一個
//若是上個幀裏的調度回調結束得早的話,那麼就有多的時間給下個幀的調度時間
activeFrameTime =
nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;
//通知已經開始幀調度了
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};
複製代碼
解析:
(1)只要調度任務不爲空,則持續調用requestAnimationFrameWithTimeout(animationTick)
這樣作的目的是:
調度隊列有多個callback
,不能保證在一個callback
完成後,恰好能在下一幀繼續執行下一個callback
,因此在當前callback
存在的同時,執行下一幀的callback
,以確保每一幀都有 React 的調度任務在執行。
(2)React 默認瀏覽器刷新頻率是 30Hz
//保持瀏覽器每秒 30 幀的狀況下,每一幀爲 33ms
let activeFrameTime = 33;
複製代碼
(3)nextFrameTime
用來計算下一幀留給 React 執行調度的時間,React 能接受最低限度的時長是 8ms,即 120Hz
(4)通知 React 調度開始執行
//通知已經開始幀調度了
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
複製代碼
port.postMessage(undefined)
是跨域通訊—MessageChannel
的使用,接下來說一下它
4、new MessageChannel()
做用:
建立新的消息通道用來跨域通訊,而且 port1 和 port2 能夠互相通訊
使用:
const channel = new MessageChannel();
const port1 = channel.port1;
const port2 = channel.port2;
port1.onmessage = function(event) {
console.log("port111111 " + event.data);
}
port2.onmessage = function(event) {
console.log("port2222 " + event.data);
}
port1.postMessage("port1發出的");
port2.postMessage("port2發出的");
複製代碼
結果:
port2222 port1發出的
port111111 port2發出的
複製代碼
能夠看到,port1 發出的消息被 port2 接收,port2 發出的消息被 port1 接收
React 源碼中的使用:
// We use the postMessage trick to defer idle work until after the repaint.
/*idleTick()*/
const channel = new MessageChannel();
const port = channel.port2;
//當調用 port.postMessage(undefined) 就會執行該方法
channel.port1.onmessage = function(event) {
isMessageEventScheduled = false;
//有調度任務的話
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
const hasTimeRemaining = frameDeadline - currentTime > 0;
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
//仍有調度任務的話,繼續執行幀調度
if (hasMoreWork) {
// Ensure the next frame is scheduled.
if (!isAnimationFrameScheduled) {
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
} else {
scheduledHostCallback = null;
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed, and post a new task as soon as possible
// so we can continue where we left off.
//若是調度任務由於報錯而中斷了,React 儘量退出當前瀏覽器執行的任務,
//繼續執行下一個調度任務
isMessageEventScheduled = true;
port.postMessage(undefined);
throw error;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
//判斷瀏覽器是否強制渲染的標誌
needsPaint = false;
}
};
複製代碼
經過port.postMessage(undefined)
就會執行該方法,判斷是否有多餘的調度任務須要被執行,若是當前調度任務報錯,就會盡量繼續執行下一個調度任務。
5、綜上
本文中的函數執行及走向,不算複雜,因此不作流程圖了,能夠綜合地看到requestHostCallback()
的做用是:
(1)在瀏覽器的刷新頻率(每一幀)內執行 React 的調度任務 callback
(2)計算每一幀中 React 進行調度任務的時長,多出的時間留給下一幀的調度任務,也就是維護時間片
(3)跨域通知 React 調度任務開始執行,並在調度任務 throw error 後,繼續執行下一個 調度任務。
(完)