事件循环

异步程序是逐片段执行的。 每个片段可能会启动一些操作,并调度代码在操作完成或失败时执行。 在这些片段之间,该程序处于空闲状态,等待下一个动作。

所以回调函数不会直接被调度它们的代码调用。 如果我从一个函数中调用setTimeout,那么在调用回调函数时该函数已经返回。 当回调返回时,控制权不会回到调度它的函数。

异步行为发生在它自己的空函数调用堆栈上。 这是没有Promise的情况下,在异步代码之间管理异常很难的原因之一。 由于每个回调函数都是以几乎为空的堆栈开始,因此当它们抛出一个异常时,你的catch处理程序不会在堆栈中。

  1. try {
  2. setTimeout(() => {
  3. throw new Error("Woosh");
  4. }, 20);
  5. } catch (_) {
  6. // This will not run
  7. console.log("Caught!");
  8. }

无论事件发生多么紧密(例如超时或传入请求),JavaScript 环境一次只能运行一个程序。 你可以把它看作在程序周围运行一个大循环,称为事件循环。 当没有什么可以做的时候,那个循环就会停止。 但随着事件来临,它们被添加到队列中,并且它们的代码被逐个执行。 由于没有两件事同时运行,运行缓慢的代码可能会延迟其他事件的处理。

这个例子设置了一个超时,但是之后占用时间,直到超时的预定时间点,导致超时延迟。

  1. let start = Date.now();
  2. setTimeout(() => {
  3. console.log("Timeout ran at", Date.now() - start);
  4. }, 20);
  5. while (Date.now() < start + 50) {}
  6. console.log("Wasted time until", Date.now() - start);
  7. // → Wasted time until 50
  8. // → Timeout ran at 55

Promise总是作为新事件来解析或拒绝。 即使已经解析了Promise,等待它会导致你的回调在当前脚本完成后运行,而不是立即执行。

  1. Promise.resolve("Done").then(console.log);
  2. console.log("Me first!");
  3. // → Me first!
  4. // → Done

在后面的章节中,我们将看到在事件循环中运行的,各种其他类型的事件。