前言:
在React源码解析之scheduleWork(下)中,我们讲到了unstable_scheduleCallback
,其中在「按计划插入调度任务」后,会调用requestHostCallback()
方法:
function unstable_scheduleCallback(priorityLevel, callback, options) {
xxx
//如果开始调度的时间已经错过了
if (startTime > currentTime) {
xxx
}
//没有延期的话,则按计划插入task
else {
xxx
//更新调度执行的标志
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
/*--------------------这里------------------*/
//执行调度 callback
requestHostCallback(flushWork);
/*-------------------------------------------*/
}
}
}
本文的目的就是讲解requestHostCallback()
的源码逻辑及其作用。
一、requestHostCallback()
作用:
执行 React 的调度任务
源码:
//在每一帧内执行调度任务(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()
函数后面解析
二、requestAnimationFrameWithTimeout()
作用:
在每一帧内执行调度任务
源码:
// 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 默认浏览器的刷新频率(下文会解释)
也就是说,如果 ① rAFID 先执行的话,即会随着浏览器刷新频率执行,并且会阻止 ② rAFTimeoutID
的执行。
② rAFTimeoutID
rAFTimeoutID
的作用更像是一个保底措施,如果 React 在进入调度流程,并且有调度队列存在,但是 100ms 仍未执行调度任务的话,则强制执行调度任务,并且阻止 ① rAFID
的执行。
也就是说 ① rAFID 和 ② rAFTimeoutID 是「竞争关系」,谁先执行,就阻止对方执行。
执行requestAnimationFrameWithTimeout
方法时,带的参数是animationTick
:
requestAnimationFrameWithTimeout(animationTick);
接下来讲下animationTick
方法
三、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
的使用,接下来讲一下它
四、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)
就会执行该方法,判断是否有多余的调度任务需要被执行,如果当前调度任务报错,就会尽可能继续执行下一个调度任务。
五、综上
本文中的函数执行及走向,不算复杂,所以不做流程图了,可以综合地看到requestHostCallback()
的作用是:
(1)在浏览器的刷新频率(每一帧)内执行 React 的调度任务 callback
(2)计算每一帧中 React 进行调度任务的时长,多出的时间留给下一帧的调度任务,也就是维护时间片
(3)跨域通知 React 调度任务开始执行,并在调度任务 throw error 后,继续执行下一个 调度任务。
(完)
网友评论