javascript - 有没有比 setTimeout(0) 更快的方式让 Javascript 事件循环?
问题描述
我正在尝试编写一个执行可中断计算的网络工作者。我知道的唯一方法(除了Worker.terminate()
)是定期屈服于消息循环,以便它可以检查是否有任何新消息。例如,这个 web worker 计算从 0 到 的整数之和data
,但是如果你在计算过程中给它发送一条新消息,它将取消计算并开始一个新的计算。
let currentTask = {
cancelled: false,
}
onmessage = event => {
// Cancel the current task if there is one.
currentTask.cancelled = true;
// Make a new task (this takes advantage of objects being references in Javascript).
currentTask = {
cancelled: false,
};
performComputation(currentTask, event.data);
}
// Wait for setTimeout(0) to complete, so that the event loop can receive any pending messages.
function yieldToMacrotasks() {
return new Promise((resolve) => setTimeout(resolve));
}
async function performComputation(task, data) {
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Yield to the event loop.
await yieldToMacrotasks();
// Check if this task has been superceded by another one.
if (task.cancelled) {
return;
}
}
// Return the result.
postMessage(total);
}
这行得通,但速度慢得惊人。在我的机器上,循环的每次迭代平均while
需要 4 毫秒!如果您想快速取消,这是一个相当大的开销。
为什么这么慢?有没有更快的方法来做到这一点?
解决方案
是的,消息队列将比超时具有更高的重要性,因此会以更高的频率触发。
您可以使用MessageChannel API轻松绑定到该队列:
let i = 0;
let j = 0;
const channel = new MessageChannel();
channel.port1.onmessage = messageLoop;
function messageLoop() {
i++;
// loop
channel.port2.postMessage("");
}
function timeoutLoop() {
j++;
setTimeout( timeoutLoop );
}
messageLoop();
timeoutLoop();
// just to log
requestAnimationFrame( display );
function display() {
log.textContent = "message: " + i + '\n' +
"timeout: " + j;
requestAnimationFrame( display );
}
<pre id="log"></pre>
现在,您可能还希望在每个事件循环中批处理几轮相同的操作。
以下是此方法有效的几个原因:
根据规范,
setTimeout
在第 5 级调用之后,即 OP 循环的第五次迭代之后,将被限制到至少 4 毫秒。
消息事件不受此限制。在某些情况下,某些浏览器会使由 发起的任务
setTimeout
具有较低的优先级。
也就是说,Firefox 在页面加载时会这样做,因此此时调用的脚本setTimeout
不会阻塞其他事件;他们甚至为此创建了一个任务队列。
即使仍未指定,似乎至少在 Chrome 中,消息事件具有“用户可见”优先级,这意味着某些 UI 事件可能首先出现,但仅此而已。(使用 Chrome 中即将推出的scheduler.postTask()
API 对此进行了测试)大多数现代浏览器会在页面不可见时限制默认超时,这甚至可能适用于 Workers。
消息事件不受此限制。正如 OP 所发现的,Chrome 确实为前 5 次调用设置了至少 1 毫秒。
但是请记住,如果所有这些限制都被设置了setTimeout
,那是因为以这样的速率调度这么多任务是有成本的。
仅在 Worker 线程中使用它!
在 Window 上下文中执行此操作将限制浏览器必须处理的所有正常任务,但他们会认为这些任务不太重要,例如网络请求、垃圾收集等。
此外,发布新任务意味着事件循环必须在频率高,永远不会闲置,这意味着更多的能量消耗。