React中requestIdleCallback的polyfill实现
最近发现了requestIdleCallback函数,也听说React源码中也适用了requestIdleCallback函数,夜来看看他在此中的作用。
实际在React源码(16.12.0)中,并没有查到该方法,查到的也只是test中用setTimeout模拟了requestIdleCallback方法。
requestIdleCallback
首先来看下这个函数,该方法会在DOM渲染空闲时期执行任务,所以是一个执行抵优先级任务的方法。
requestIdleCallback(function(deadline) {
console.log(`剩余空闲时间:${ deadline.timeRemaining() }`);
console.log(`是否是超时执行:${ deadline.didTimeout }`);
});
deadline.timeRemaining()
输出了每帧剩余的渲染时间,如果没有剩余时间,此回调不会在当前帧中执行,留到下次执行,此时就超时了,didTimeout
即为true。但是为啥在React中没有直接使用,而是polyfill了呢?
在chrome文档上,可以看到原生提供的requestIdleCallback方法的timeRemaining()最大返回是50ms,也就是20fps,达不到页面流畅度的要求,并且该API兼容性也比较差。
SchedulerHostConfig.default.js
该文件位于react/packages/scheduler/src/forks
下,他实现了对requestIdleCallback的polyfill。
他针对DOM下和非DOM环境下实现了两套方案,我们先来看下该文件暴露出去的方法。
export let requestHostCallback; // requestIdleCallback的polyfill方法
export let cancelHostCallback; // 用于取消requestHostCallback
export let requestHostTimeout;
export let cancelHostTimeout; // 用于取消requestHostTimeout
export let shouldYieldToHost;
export let requestPaint;
export let getCurrentTime; // 获取当前触发事件
export let forceFrameRate; // 设置渲染的fps
非DOM环境
getCurrentTime
initialTime记录了一开始的触发时间戳,getCurrentTime函数则返回当前时间戳减去初始时间戳。
const initialTime = Date.now();
getCurrentTime = function() {
return Date.now() - initialTime;
};
requestHostCallback
const _flushCallback = function() {
if (_callback !== null) {
// 无函数时不执行
try {
const currentTime = getCurrentTime();
const hasRemainingTime = true;
_callback(hasRemainingTime, currentTime);
_callback = null;
} catch (e) {
setTimeout(_flushCallback, 0);
throw e;
}
}
};
requestHostCallback = function(cb) {
if (_callback !== null) {
// 利用setTimeout的第三个参数
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0);
}
};
requestHostCallback是基于setTimeout实现的,主要调用位于_flushCallback函数体内,当存在_callback时,便会触发_callback,传入true表示未超时,仍有空闲时间,currentTime则表示当前触发的时间戳。
requestHostCallback中,若存在_callback时,表示先前还有任务未完成,便利用setTimeout的第三个参数延后执行任务。
注:有关setTimeout的第三个参数可以见:setTimeout第三个参数。
cancelHostCallback
cancelHostCallback函数是用来取消requestHostCallback定时的。将函数设置置空,即可。
cancelHostCallback = function() {
_callback = null; // 置空,不执行
};
requestHostTimeout和cancelHostTimeout
requestHostTimeout = function(cb, ms) {
_timeoutID = setTimeout(cb, ms);
};
cancelHostTimeout = function() {
clearTimeout(_timeoutID);
};
shouldYieldToHost
表示触发是否超时,即当前帧是否过期。非DOM环境下一直都是false。
requestPaint 和 forceFrameRate
非DOM下无实质用处,为空函数。
DOM环境下
if (typeof console !== 'undefined') {
const requestAnimationFrame = window.requestAnimationFrame;
const cancelAnimationFrame = window.cancelAnimationFrame;
// ....
}
首先是对requestAnimationFrame
和cancelAnimationFrame
两方法做了是否有的判断,并做了error的提示,实际上,在本文件下是没有使用这两个方法的,注释是说以防未来可能有使用。(看了先前版本的代码,是有基于requestAnimationFrame实现的。)
getCurrentTime
在DOM环境下,优先使用performance下的now方法,不支持的情况下,使用Date方式。
if (
typeof performance === 'object' &&
typeof performance.now === 'function'
) {
getCurrentTime = () => performance.now();
} else {
const initialTime = Date.now();
getCurrentTime = () => Date.now() - initialTime;
}
forceFrameRate
该方法用于设置渲染的fps。
forceFrameRate = function(fps) {
if (fps < 0 || fps > 125) {
console.error(
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing framerates higher than 125 fps is not unsupported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
yieldInterval = 5;
}
};
fps仅支持0-125fps的,超出的做error提示。yieldInterval就是每帧时间。
requestHostCallback
在非DOM下,requestHostCallback使用的是基于setTimeout实现的,而在DOM下是基于postMessage和onmessage(MessageChannel)这个发布订阅模式做的。
postMessage
先看下requestHostCallback函数主体,
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
将回调函数存储起来,触发postMessage发送消息。isMessageLoopRunning表示是否在postMessage处理中,即是否在时间处理中。
onmessage
postMessage后,触发performWorkUntilDeadline函数,
channel.port1.onmessage = performWorkUntilDeadline;
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
// 有回调任务
const currentTime = getCurrentTime();
deadline = currentTime + yieldInterval; // 设置deadline
const hasTimeRemaining = true; // 任务有剩余时间
try {
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
port.postMessage(null);
}
} catch (error) {
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
yieldInterval
是根据fps算出的每帧时间,超过这个时间就会影响帧数,到达不了想要的结果,所以设置了个deadline
,即最终期限时间。
hasTimeRemaining
表示是否有剩余处理时间,即未到达deadline
。
当执行完scheduledHostCallback后,还有任务时,继续触发postMessage。没有的话,清空掉回调函数。
cancelHostCallback
置空回调,这样在performWorkUntilDeadline中就不会执行对应的函数了,相当于取消了requestHostCallback。
requestHostTimeout
requestHostTimeout = function(callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
基于setTimeout,向回调传入当前时间作为参数。
cancelHostTimeout
requestHostTimeout的取消方法,即clearTimeout。
shouldYieldToHost和requestPaint
isInputPending的内容,可以看下这篇文章(is-input-pending)[https://wicg.github.io/is-input-pending/],目前这只是个提议,还没有DOM是支持的。
而在SchedulerFeatureFlags.js
中enableIsInputPending也是被定义为false,故函数定义位于else中。
shouldYieldToHost = function() {
return getCurrentTime() >= deadline;
};
requestPaint = function() {};
shouldYieldToHost根据当前时间是否大于最后的预期时间。requestPaint和在非DOM下一样,都是未实现的空函数。