面试必看:深入浅出 JavaScript 事件循环与异步编程技巧

引言

JavaScript 是单线程的,但它却能同时处理成千上万的异步任务,这背后的秘密就是事件循环(Event Loop) 。事件循环就像一位高效的时间管理大师,它通过巧妙的调度机制,让 JavaScript 在单线程环境下也能实现非阻塞的异步操作。

今天,我们就来揭开事件循环的神秘面纱,看看它是如何工作的,以及如何利用它写出高效的异步代码。

事件循环的基本概念

1. 调用栈(Call Stack)

调用栈是 JavaScript 执行同步代码的地方。它是一个后进先出(LIFO)的数据结构,用来记录函数的调用顺序。每当一个函数被调用时,它就会被推入调用栈;当函数执行完毕后,它就会从调用栈中弹出。

举个例子:

javascript 复制代码
function foo() {
  console.log('foo');
}

function bar() {
  foo();
  console.log('bar');
}

bar();

调用栈的执行顺序:

  1. bar() 被推入调用栈。
  2. foo() 被推入调用栈。
  3. console.log('foo') 执行并弹出。
  4. foo() 执行完毕并弹出。
  5. console.log('bar') 执行并弹出。
  6. bar() 执行完毕并弹出。

2. 任务队列(Task Queue)

任务队列用于存放异步任务的回调函数。当异步任务(如 setTimeoutsetInterval、I/O 操作等)完成时,它们的回调函数会被放入任务队列中,等待事件循环处理。

3. 宏任务(Macrotask)

宏任务是事件循环中的主要任务单元。每次事件循环会执行一个宏任务,然后检查并执行所有的微任务。

常见的宏任务包括:
  • 定时器任务setTimeoutsetInterval
  • I/O 操作:如文件读写、网络请求等。
  • UI 渲染:浏览器中的 UI 更新。
  • 事件回调 :如 clickscroll 等 DOM 事件。
  • requestAnimationFrame:浏览器中的动画帧回调(优先级高于普通宏任务)。
  • setImmediate(Node.js 特有):在当前事件循环结束后立即执行。

4. 微任务队列(Microtask Queue)

微任务队列用于存放优先级更高的异步任务回调,微任务会在当前任务执行完毕后立即执行且会在下一个宏任务开始之前清空。

常见的微任务包括:
  • Promise 的回调 :如 thencatchfinally
  • queueMicrotask:用于将任务推入微任务队列的 API。
  • MutationObserver:监听 DOM 变化的回调。
  • process.nextTick (Node.js 特有):在当前任务结束后立即执行,优先级高于 Promise

事件循环的工作流程

事件循环的工作流程可以概括为以下几个步骤:

  1. 执行同步代码:从调用栈中执行同步任务。
  2. 清空微任务队列 :执行所有微任务(如 Promise 的回调)。
  3. 执行一个宏任务:从任务队列中取出一个宏任务执行。
  4. 重复上述过程:不断循环,直到所有任务完成。

示例:事件循环的执行顺序

让我们通过一个经典的例子来理解事件循环的执行顺序:

javascript 复制代码
console.log('Script start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('Script end');

输出顺序:

sql 复制代码
Script start
Script end
Promise
Timeout

解释:

  1. 同步代码console.log('Script start')console.log('Script end') 被依次执行。
  2. 微任务Promise.resolve().then() 的回调被放入微任务队列,并在同步代码执行完毕后立即执行,输出 Promise
  3. 宏任务setTimeout 的回调被放入任务队列,在微任务执行完毕后执行,输出 Timeout

事件循环的应用场景

1. 异步编程

事件循环是 JavaScript 异步编程的核心。通过 Promiseasync/await 等工具,我们可以轻松地编写非阻塞的异步代码。

2. 性能优化

理解事件循环有助于优化代码性能。例如,将耗时任务拆分为多个微任务,可以避免阻塞主线程。

3. 避免竞态条件

通过合理使用微任务和宏任务,可以避免异步操作中的竞态条件,确保代码的正确性。

事件循环的进阶知识

1. requestAnimationFrame

requestAnimationFrame 是一个特殊的宏任务,用于在浏览器下一次重绘之前执行动画更新。它的优先级高于普通的宏任务,但低于微任务。

2. queueMicrotask

queueMicrotask 是一个用于将任务推入微任务队列的 API。它可以替代 Promise.resolve().then(),提供更直观的微任务调度方式。

3. Node.js 中的事件循环

在 Node.js 中,事件循环的实现与浏览器略有不同。Node.js 的事件循环分为多个阶段,每个阶段处理不同类型的任务。

总结:事件循环------异步编程的基石

JavaScript 的事件循环机制是其异步编程模型的核心。通过理解调用栈、任务队列和微任务队列的工作原理,我们可以更好地掌握异步代码的执行顺序,避免常见的并发问题。

记住以下几点:

  • 同步代码优先执行
  • 微任务优先于宏任务
  • 合理使用 Promiseasync/await,可以让你的代码更高效、更易读。

事件循环就像一位幕后英雄,默默地为 JavaScript 的异步世界提供动力。掌握了它,你就能在异步编程的海洋中游刃有余!

小测验:

javascript 复制代码
console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise 1');
}).then(() => {
  console.log('Promise 2');
});

console.log('End');

输出顺序是什么?

(答案:StartEndPromise 1Promise 2Timeout

希望这篇文章能让你对 JavaScript 的事件循环有更深入的理解!如果你有任何问题,欢迎留言讨论~ 😄

相关推荐
uperficialyu7 分钟前
2025年01月10日浙江鑫越系统科技前端面试
前端·科技·面试
付朝鲜32 分钟前
用自写的jQuery库+Ajax实现了省市联动
java·前端·javascript·ajax·jquery
coderYYY40 分钟前
多个el-form-item两列布局排齐且el-select/el-input组件宽度撑满
前端·javascript·vue.js·elementui·前端框架
荔枝吖1 小时前
项目中会出现的css样式
前端·css·html
Dontla1 小时前
何时需要import css文件?怎么知道需要导入哪些css文件?为什么webpack不提示CSS导入?(导入css导入规则、css导入规范)
前端·css·webpack
小堃学编程1 小时前
前端学习(2)—— CSS详解与使用
前端·css·学习
蓝婷儿2 小时前
第一章:HTML基石·现实的骨架
前端·html
Watermelo6172 小时前
前端如何应对精确数字运算?用BigNumber.js解决JavaScript原生Number类型在处理大数或高精度计算时的局限性
开发语言·前端·javascript·vue.js·前端框架·vue·es6
HebyH_2 小时前
2025前端面试遇到的问题(vue+uniapp+js+css)
前端·javascript·vue.js·面试·uni-app
Clockwiseee2 小时前
CSRF记录
前端·csrf