深入理解 JavaScript 事件循环:超越 setTimeout

调用栈与任务队列

JavaScript 是单线程的,这意味着它只有一个调用栈来执行代码。当函数被调用时,它会被推入调用栈;当函数执行完毕后,它会被弹出。然而,像网络请求或定时器这样的耗时操作如果都在主线程上执行,将会阻塞整个页面。为了解决这个问题,JavaScript 使用了一个任务队列。当一个异步操作(如 setTimeout)完成时,它的回调函数会被放入任务队列。事件循环会持续检查调用栈是否为空,如果为空,它就会从任务队列中取出第一个任务,并将其推入调用栈执行。83wl2.tongdaolzw.com

console.log("Start");

setTimeout(function() {
  console.log("Timeout callback");
}, 2000);

console.log("End");

// 输出顺序:
// 1. "Start"
// 2. "End"
// 3. (大约 2 秒后) "Timeout callback"

微任务与宏任务kqyfr9.tongdaolzw.com

任务队列实际上被分为两种:宏任务和微任务。宏任务包括 setTimeoutsetInterval、I/O 操作和 UI 渲染等。微任务则包括 Promise 的回调(.then(), .catch(), .finally())以及 queueMicrotask()。事件循环的执行规则至关重要:在执行完一个宏任务后,会立即清空所有微任务队列中的任务,然后再执行下一个宏任务。这就解释了为什么 Promise 的回调总是比 setTimeout 的回调先执行,即使 setTimeout 的延迟时间为 0。8ketm.tongdaolzw.com

console.log("Script Start");

setTimeout(function() {
  console.log("setTimeout (Macro-task)");
}, 0);

Promise.resolve().then(function() {
  console.log("Promise.then (Micro-task)");
});

console.log("Script End");

// 输出顺序:
// 1. "Script Start"
// 2. "Script End"
// 3. "Promise.then (Micro-task)"
// 4. "setTimeout (Macro-task)"

async/await 的底层魔法2t926.tongdaolzw.com

async/await 是 JavaScript 中处理异步的语法糖,它让异步代码看起来像同步代码。但它的底层仍然是基于 Promise 的。一个 async 函数会隐式地返回一个 Promise。当函数执行到 await 关键字时,它会暂停该函数的执行,等待 await 后面的 Promise 完成。重要的是,await 后面的代码实际上相当于被放到了 .then() 的回调中,因此它是一个微任务i4xae.tongdaolzw.com

async function asyncFunction() {
  console.log("Inside async function (before await)");
  await Promise.resolve();
  console.log("Inside async function (after await)");
}

console.log("Script Start");

asyncFunction();

console.log("Script End");

// 输出顺序:
// 1. "Script Start"
// 2. "Inside async function (before await)"
// 3. "Script End"
// 4. "Inside async function (after await)"

渲染任务的角色o9ei6.tongdaolzw.com

在浏览器环境中,事件循环还有一个重要的参与者:渲染任务。浏览器通常希望在每次宏任务执行完毕后,并在执行下一个宏任务之前,重新渲染页面。这意味着,如果你有一个执行时间很长的宏任务(例如一个巨大的 for 循环),它会阻塞 UI 的更新,导致页面卡顿。将复杂的计算分解成多个小的宏任务(例如使用 setTimeout(fn, 0))可以让浏览器有机会在间隙中渲染 UI,从而保持页面的响应性。gdn29.tongdaolzw.com

// 一个会阻塞 UI 的同步任务
function blockingTask() {
  const start = Date.now();
  while (Date.now() - start < 3000) {
    // 空循环,持续 3 秒
  }
  console.log("Blocking task finished");
}

// 在控制台运行 blockingTask(),你会发现页面在这 3 秒内是完全无响应的。
// 而微任务和宏任务机制的存在,就是为了避免这种情况的发生。

整合所有部分:一个复杂示例eppq7.tongdaolzw.com

现在,让我们把所有概念整合起来,分析一个更复杂的例子。你需要记住以下执行顺序:1. 执行同步代码。2. 执行所有微任务。3. 执行一个宏任务。4. 重复步骤 2 和 3。通过这个模型,你可以精确地预测任何异步 JavaScript 代码的执行顺序。d6gys.tongdaolzw.com

console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => {
  console.log("3");
  Promise.resolve().then(() => console.log("4"));
});

Promise.resolve().then(() => console.log("5"));

setTimeout(() => console.log("6"), 0);

console.log("7");

// 正确的输出顺序是:
// 1. "1" (同步)
// 2. "7" (同步)
// 3. "3" (第一个微任务)
// 4. "5" (第二个微任务)
// 5. "4" (在微任务 "3" 中产生的微任务)
// 6. "2" (第一个宏任务)
// 7. "6" (第二个宏任务)

全部评论

相关推荐

屋顶的闪闪星光:1、是否舒服,有很大的不确定性,在开奖之前就是0和1,但是企业背书、待遇这些差距是确定性的。凡事都是个赌,所以选字节吧。 2、多讲几句,前端被AI替代的速度太恐怖了,要么向大前端(H5、小程序、Android、iOS、鸿蒙等)方向转型,要么向“产品交互设计 + 前端实现”两条腿走路。字节的视野更开阔。
投递哔哩哔哩等公司7个岗位
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务