模拟
2024年03月06日
一、setTimeout
1.1 认识
使用setTimeout
模拟相对简单,但精度较低,因为setTimeout
的调度并不保证精确。这种方法周期性检查是否有足够的空闲时间来执行回调(通常以50
毫秒为阈值),如果超时,那么无论如何都会执行该回调。
1.2 实现
function requestIdleCallbackOfSetTimeout() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const callbackWrapper = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
window.setTimeout(callbackWrapper);
}
};
window.setTimeout(callbackWrapper);
};
}
二、MessageChannel
2.1 认识
这种方法利用MessageChannel
来创建一个微任务,这个微任务会试图在每帧结束时运行,从而模拟出requestIdleCallback
的行为。这种方法相比setTimeout
有更好的精确度。
2.2 实现
function requestIdleCallbackOfMessageChannel() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const channel = new MessageChannel();
channel.port2.onmessage = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
channel.port1.postMessage(undefined);
}
};
channel.port1.postMessage(undefined);
};
}
三、requestIdleCallback polyfill
3.1 认识
return window.requestIdleCallback.bind(window)
而不是直接return window.requestIdleCallback
的原因: 直接返回window.requestIdleCallback
有可能会在没有正确上下文的情况下使用这个函数,导致Uncaught TypeError: Illegal invocation
错误。要修复这个问题,确保当你尝试返回window.requestIdleCallback
或setTimeout
时,使用一个函数包装它们,以保持正确的上下文。这可以通过使用.bind()
方法实现
3.2 实现
function requestIdleCallbackOfSetTimeout() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const callbackWrapper = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
window.setTimeout(callbackWrapper);
}
};
window.setTimeout(callbackWrapper);
};
}
function requestIdleCallbackOfMessageChannel() {
return (cb, { timeout } = {}) => {
const start = performance.now();
const channel = new MessageChannel();
channel.port2.onmessage = () => {
const now = performance.now();
const didTimeout = timeout && now - start >= timeout;
if (didTimeout || now - start < 50) {
cb({
didTimeout,
timeRemaining() { return Math.max(0, 50 - (now - start)); },
});
} else {
channel.port1.postMessage(undefined);
}
};
channel.port1.postMessage(undefined);
};
}
function polyfillRequestIdleCallback() {
if (typeof window.requestIdleCallback === 'function') {
return window.requestIdleCallback.bind(window);
} if (typeof MessageChannel === 'function') {
return requestIdleCallbackOfMessageChannel();
}
return requestIdleCallbackOfSetTimeout();
}