Background Tasks API
::: tip
文章参考自MDN。后台任务协作调度 API(Cooperative Scheduling of Background Tasks API,也叫后台任务 API,或者简单称为 requestIdleCallback() API)提供了由用户代理决定的,在空闲时间自动执行队列任务的能力。
:::
浏览器的主线程中心逻辑就是事件循环,主线程需要做的事情太多,其中事件处理和屏幕更新是用户关注性能最明显的两种方式,对于应用来说,防止在事件队列中出现卡顿是很重要的。
后台任务API提供了一个新的接口:IdleDeadline,idle callback 旨在为代码提供一种与事件循环协作的方式,以确保系统充分利用其潜能,不会过度分配任务,从而导致延迟或其他性能问题,因此你应该考虑如何使用它。这个 API 给 Window 接口增加了新的 requestIdleCallback() 和 cancelIdleCallback() 方法。
- 对非高优先级的任务使用空闲回调
- 空闲回调应尽可能不超支分配到的时间(50 ms 的上限时间)
- 避免在空闲回调中改变 DOM(使用Window.requestAnimationFrame())
- 避免运行时间无法预测的任务
- 在你需要的时候要用 timeout,但记得只在需要的时候才用
使用兼容判断
因为后台任务 API 还是相当新的,而你的代码可能需要在那些不仍不支持此 API 的浏览器上运行。你可以把 setTimeout() 用作回调选项来做浏览器不支持时的补丁判断。
window.requestIdleCallback =
window.requestIdleCallback ||
function (handler) {
let startTime = Date.now();
return setTimeout(function () {
handler({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50.0 - (Date.now() - startTime));
},
});
}, 1);
};
取消创建的后台任务补丁:
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
};
综合案例
<html>
<p>使用 <code>requestIdleCallback()</code> 方法的后台任务协作调度演示。</p>
<div id="container">
<div class="label">解码量子丝极谱发射中...</div>
<progress id="progress" value="0"></progress>
<div class="button" id="startButton">开始</div>
<div class="label counter">
任务 <span id="currentTaskNumber">0</span> /
<span id="totalTaskCount">0</span>
</div>
</div>
<div id="logBox">
<div class="logHeader">记录</div>
<div id="log"></div>
</div>
</html>
<style>
#logBox {
margin-top: 16px;
width: 400px;
height: 500px;
border-radius: 6px;
border: 1px solid black;
box-shadow: 4px 4px 2px black;
}
.logHeader {
margin: 0;
padding: 0 6px 4px;
height: 22px;
background-color: lightblue;
border-bottom: 1px solid black;
border-radius: 6px 6px 0 0;
}
#log {
font:
12px "Courier",
monospace;
padding: 6px;
overflow: auto;
overflow-y: scroll;
width: 388px;
height: 460px;
}
#container {
width: 400px;
padding: 6px;
border-radius: 6px;
border: 1px solid black;
box-shadow: 4px 4px 2px black;
display: block;
overflow: auto;
}
.label {
display: inline-block;
}
.counter {
text-align: right;
padding-top: 4px;
float: right;
}
.button {
padding-top: 2px;
padding-bottom: 4px;
width: 100px;
display: inline-block;
float: left;
border: 1px solid black;
cursor: pointer;
text-align: center;
margin-top: 0;
color: white;
background-color: darkgreen;
}
#progress {
width: 100%;
padding-top: 6px;
}
</style>
<script>
const taskList = [];
let totalTaskCount = 0;
let currentTaskNumber = 0;
let taskHandle = null;
const totalTaskCountElem = document.getElementById("totalTaskCount");
const currentTaskNumberElem = document.getElementById("currentTaskNumber");
const progressBarElem = document.getElementById("progress");
const startButtonElem = document.getElementById("startButton");
const logElem = document.getElementById("log");
let logFragment = null;
let statusRefreshScheduled = false;
requestIdleCallback =
requestIdleCallback ||
((handler) => {
const startTime = Date.now();
return setTimeout(() => {
handler({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50.0 - (Date.now() - startTime));
},
});
}, 1);
});
cancelIdleCallback =
cancelIdleCallback ||
((id) => {
clearTimeout(id);
});
function enqueueTask(taskHandler, taskData) {
taskList.push({
handler: taskHandler,
data: taskData,
});
totalTaskCount++;
if (!taskHandle) {
taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
}
scheduleStatusRefresh();
}
function runTaskQueue(deadline) {
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
taskList.length
) {
const task = taskList.shift();
currentTaskNumber++;
task.handler(task.data);
scheduleStatusRefresh();
}
if (taskList.length) {
taskHandle = requestIdleCallback(runTaskQueue, { timeout: 1000 });
} else {
taskHandle = 0;
}
}
function scheduleStatusRefresh() {
if (!statusRefreshScheduled) {
requestAnimationFrame(updateDisplay);
statusRefreshScheduled = true;
}
}
function updateDisplay() {
const scrolledToEnd =
logElem.scrollHeight - logElem.clientHeight <= logElem.scrollTop + 1;
if (totalTaskCount) {
if (progressBarElem.max !== totalTaskCount) {
totalTaskCountElem.textContent = totalTaskCount;
progressBarElem.max = totalTaskCount;
}
if (progressBarElem.value !== currentTaskNumber) {
currentTaskNumberElem.textContent = currentTaskNumber;
progressBarElem.value = currentTaskNumber;
}
}
if (logFragment) {
logElem.appendChild(logFragment);
logFragment = null;
}
if (scrolledToEnd) {
logElem.scrollTop = logElem.scrollHeight - logElem.clientHeight;
}
statusRefreshScheduled = false;
}
function log(text) {
if (!logFragment) {
logFragment = document.createDocumentFragment();
}
const el = document.createElement("div");
el.textContent = text;
logFragment.appendChild(el);
}
function logTaskHandler(data) {
log(`运行任务 #${currentTaskNumber}`);
for (let i = 0; i < data.count; i += 1) {
log(`${(i + 1).toString()}. ${data.text}`);
}
}
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function decodeTechnoStuff() {
totalTaskCount = 0;
currentTaskNumber = 0;
updateDisplay();
const n = getRandomIntInclusive(100, 200);
for (let i = 0; i < n; i++) {
const taskData = {
count: getRandomIntInclusive(75, 150),
text: `该文本来自任务 ${i + 1}/${n}`,
};
enqueueTask(logTaskHandler, taskData);
}
}
document
.getElementById("startButton")
.addEventListener("click", decodeTechnoStuff, false);
</script>