我正在研究 Javascript 事件循环如何工作以供将来的演示使用,但根据目前掌握的信息,我遇到了意外的行为。简单来说,当我向worker发送消息时,postmessage的执行总是发生在setTimeout之后..
new Worker('worker.js').postMessage('');
setTimeout( () => {
const now = new Date().toISOString();
console.log(`setTimeout execution: ${now}`);
}, 0);
onmessage = function (event) {
const now = new Date().toISOString();
console.log(`Worker execution: ${now}`);
}
const workerCode = `
self.onmessage = function (event) {
const now = new Date().toISOString();
self.postMessage("worker executed: " + now);
}`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerURL = URL.createObjectURL(blob);
const worker = new Worker(workerURL);
worker.onmessage = function (event) {console.log(event.data)};
worker.postMessage('');
setTimeout( () => {
const now = new Date().toISOString();
console.log(`setTimeout executed: ${now}`);
}, 0);
如果我错了,请纠正我,但事件循环按以下顺序工作:
如果确实如此,知道setTimeout是放在宏任务队列中的,也就是字面意思是最后一个读取的队列,那为什么postmessage会在aftersetTimeout之后执行呢?如果在同步执行期间,在setTimeout之前将其放入队列(无论该队列是什么)?
我尝试延迟,但即使 setTimeout 延迟,这种情况仍然会发生,所以我相信造成这种情况的原因并不是工作线程在与主线程并行的线程中运行。示例:
setTimeout( () => {
const start = performance.now();
while (performance.now() - start < 1000) { };
const now = new Date().toISOString();
console.log(`Execution setTimeout: ${now}`);
}, 0);
如果我错了,请纠正我,但事件循环按以下顺序工作:
- 执行调用堆栈中的所有任务
- 执行微任务中的所有任务
- 在宏任务中执行任务
- 渲染图形/UI
不完全是这样,尽管这对于以下内容来说并不重要。
首先,将调用堆栈上的内容称为“任务”是相当混乱的。根据规范命名法,任务是一个规范构造,告诉浏览器如何执行某些操作。 调用堆栈上有 JS 脚本执行状态(函数、领域、作用域等)。
但是,是的,一旦调用堆栈为空,即当所有 JS 从上到下执行完时,就会执行微任务检查点。
请注意,每个task可能会发生多次,并且某些任务可能不执行任何JS。所以tasks和微任务之间没有直接关系。唯一的关系是它们都是告诉浏览器执行某些操作的规范构造。
现在,渲染是作为“更新渲染”特殊任务的一部分执行的。请注意,很长一段时间以来,它一直被认为是事件循环的特殊部分,直到最近我们才将其更新为实际任务,就像大多数浏览器实际实现的那样。
这个特殊任务负责运行一些回调(这将全部触发微任务检查点),然后将继续进行实际渲染。它通常在任务优先级系统中具有最高优先级之一,尽管没有真正指定。
task-sourcesetTimeout
是放在宏任务队列中的,也就是字面意思是最后一个读取的队列,为什么
会在postMessage
之后执行呢?如果在同步执行期间,它在
setTimeout
?
setTimeout
之前被放入队列(无论该队列是什么)尽管事件循环中有很多队列,但仍然只有一个事件循环(每个上下文)。因此,即使有优先级系统,每个队列的优先级也没有指定,浏览器可以自由选择他们想要执行的任何任务,只要它是自己排队的最旧的任务
(请注意,任务源不是任务队列,多个任务源可能会出现在同一个队列中)。
每个MessagePort
实际上都有自己的
自己的任务源,并且
setTimeout
任务在计时器任务源上排队。同样,它们的优先级没有指定,并且通常具有相同的优先级,这对应于传入的 优先任务调度 API 中的
"user-visible"
,因此从 setTimeout(fn, 0)
排队的任务和消息事件之间的顺序未设置且无法假设。浏览器可以在这里自由地做任何他们想做的事。
在您的情况下,您正在生成一个新的 Worker,它将有
自己的事件循环。
您缺少的是,虽然 Worker
对象是同步可用的,但实际的工作线程却不是。浏览器首先必须获取脚本(异步),然后它必须生成一个新的上下文(异步),一旦它启动并运行,它将检查是否收到来自主线程的传入消息,最后执行这些消息。
OffscreenCanvas
,至少 Chrome 会锁定 Worker 的执行,直到生成它的主线程结束当前任务,这是因为它们需要在两个上下文之间同步渲染)。