跳到主要内容

模拟

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 认识

  1. return window.requestIdleCallback.bind(window) 而不是直接 return window.requestIdleCallback 的原因: 直接返回window.requestIdleCallback 有可能会在没有正确上下文的情况下使用这个函数,导致Uncaught TypeError: Illegal invocation错误。要修复这个问题,确保当你尝试返回window.requestIdleCallbacksetTimeout时,使用一个函数包装它们,以保持正确的上下文。这可以通过使用.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();
}