JavaScript事件循环难理解?从执行顺序入手彻底搞懂运行机制

JavaScript事件循环:从执行顺序入手彻底搞懂运行机制

大家好,今天我们来深入探讨一个让无数JavaScript开发者感到困惑,但又至关重要的概念——事件循环(Event Loop)。你可能听说过它,也可能在面试中被问到过,甚至在调试异步代码时被它的行为搞得一头雾水。别担心,这很正常。事件循环的复杂性在于它不像同步代码那样直观,它的运作机制隐藏在JavaScript运行时的幕后。

然而,理解事件循环并非高不可攀。今天,我们将采用一种自底向上、层层递进的方式,从最基本的执行顺序入手,彻底揭开它的神秘面纱。一旦你掌握了代码是如何被调度和执行的,你将能够准确预测异步代码的行为,写出更高效、更健壮、更易于调试的JavaScript应用。

1. JavaScript的单线程本质:同步执行的基础

要理解事件循环,我们首先必须牢记JavaScript的一个核心特性:它是单线程的。这意味着在任何给定的时间点,JavaScript引擎只能执行一个任务。它没有并行处理任务的能力,所有任务都必须排队等待执行。

这种单线程模型带来了一个显而易见的问题:如果一个任务耗时过长,比如进行复杂的计算,或者等待网络请求的响应,那么整个程序就会被“卡住”,用户界面(UI)会冻结,用户体验将变得非常糟糕。这显然是不可接受的。

为了解决这个问题,JavaScript引入了异步编程的概念,而事件循环正是实现异步编程的核心机制。但在我们深入异步之前,让我们先了解同步代码是如何在单线程环境中运行的。

1.1. 调用栈(Call Stack):同步代码的舞台

JavaScript引擎使用一个称为“调用栈”(Call Stack)的数据结构来管理函数的执行。调用栈是一个后进先出(LIFO)的栈。当一个函数被调用时,它会被推入栈顶;当函数执行完毕后,它会从栈中弹出。

让我们看一个简单的例子:

function multiply(a, b) {
    return a * b;
}

function square(n) {
    return multiply(n, n);
}

function printSquare(n) {
    const result = square(n);
    console.log(`The square of ${n} is ${result}`);
}

printSquare(5);
console.log("Global scope finished.");

这段代码的执行流程如下:

  1. printSquare(5) 被调用,推入调用栈。
  2. printSquare 内部,square(5) 被调用,推入调用栈(在 printSquare 之上)。
  3. square 内部,multiply(5, 5) 被调用,推入调用栈(在 square 之上)。
  4. multiply 函数执行 5 * 5,返回 25multiply 从调用栈弹出。
  5. square 函数接收到 25,将其赋值给 result,然后返回 25square 从调用栈弹出。
  6. printSquare 函数接收到 25,将其赋值给 result,然后执行 console.logprintSquare 从调用栈弹出。
  7. 全局代码继续执行 console.log("Global scope finished.")
  8. 所有代码执行完毕,调用栈为空。

调用栈状态变化(简化):

步骤 调用栈 备注
1 printSquare printSquare(5) 被调用
2 square square(5) 被调用
3 multiply multiply(5, 5) 被调用
4 square multiply 完成,弹出
5 printSquare square 完成,弹出
6 printSquare 完成,弹出
7 console.log 全局 console.log 被调用
8 全局 console.log 完成,栈清空

关键点: 只要调用栈不为空,JavaScript引擎就会一直执行当前栈顶的函数。这意味着任何同步代码都会阻塞后续代码的执行,直到它完成。

2. 浏览器环境:Web APIs的引入

JavaScript引擎本身只负责执行JS代码。然而,我们的JavaScript程序通常运行在浏览器或Node.js这样的宿主环境中。这些宿主环境提供了JavaScript引擎无法独立完成的功能,比如:

  • DOM操作: 改变网页结构、样式和内容。
  • 定时器: setTimeoutsetInterval
  • 网络请求: XMLHttpRequestfetch
  • 文件操作: FileReader
  • 本地存储: localStorage

这些功能被称为 Web APIs(在浏览器环境中)或 Node.js APIs(在Node.js环境中)。它们不是JavaScript语言的一部分,而是由宿主环境提供的接口。

重要的是,这些Web APIs是异步的!当JavaScript代码调用一个Web API时,该API会把任务交给浏览器(或Node.js)的底层系统去处理,而JavaScript引擎本身并不会等待这个任务完成。相反,它会继续执行调用栈中的下一个任务。当Web API的任务完成后,它会将一个回调函数(callback function)放入一个特殊的队列中,等待JavaScript引擎来执行。

这就是异步编程的开始,也是事件循环登场的舞台。

3. 事件循环的核心组件

事件循环并不是一个单一的实体,而是一个协调JavaScript引擎、Web APIs和各种任务队列的机制。它由以下几个核心组件构成:

  1. 调用栈(Call Stack): 我们已经介绍过,用于执行同步代码。
  2. Web APIs: 浏览器提供的异步功能接口,如 setTimeout, fetch, DOM事件 等。当JS代码调用这些API时,它们会启动一个异步操作,并将对应的回调函数注册到内部。
  3. 任务队列(Task Queue / Macrotask Queue): 当Web APIs启动的异步操作完成后,其对应的回调函数不会立即执行,而是会被放入这个队列中排队。例如,setTimeout 的回调、DOM事件的回调、fetch 请求成功或失败的回调等。
  4. 微任务队列(Microtask Queue): 这是一个比任务队列优先级更高的队列。它主要用于处理 Promise 的回调 (.then(), .catch(), .finally()) 和 MutationObserver 的回调。
  5. 事件循环(Event Loop): 这是一个持续运行的进程,它负责监控调用栈和任务队列。当调用栈为空时,事件循环会检查任务队列,并将其中的回调函数推入调用栈执行。

核心机制概览:

  1. 所有同步代码在调用栈中按顺序执行。
  2. 当遇到异步Web API调用时,该异步操作被Web APIs模块处理,其回调函数被注册。
  3. 异步操作完成后,对应的回调函数被放入任务队列(或微任务队列)。
  4. 事件循环不断检查调用栈是否为空。
  5. 如果调用栈为空,事件循环首先会清空微任务队列中的所有任务,将它们推入调用栈执行。
  6. 微任务队列清空后,事件循环会从任务队列中取出一个(注意:是“一个”)任务,将其推入调用栈执行。
  7. 然后,重复步骤4-6。

4. 深入事件循环的执行顺序:宏任务与微任务

理解事件循环的关键在于区分“宏任务”(Macrotasks)和“微任务”(Microtasks),以及它们在执行顺序上的优先级。

4.1. 宏任务(Macrotasks / Tasks)

宏任务代表了独立的、离散的工作单元。每次事件循环迭代(一个“tick”)通常会处理一个宏任务。

常见的宏任务包括:

  • 整个 script 标签的执行(初始化代码)。
  • setTimeout() 的回调。
  • setInterval() 的回调。
  • requestAnimationFrame() 的回调(虽然它有特殊的渲染时机,但通常被归类为一种特殊的宏任务,与浏览器渲染帧同步)。
  • I/O 操作(如网络请求完成后的回调)。
  • UI 渲染事件(如点击、键盘输入)。
  • MessageChannel 的回调。
  • postMessage 的回调。

4.2. 微任务(Microtasks)

微任务是比宏任务更小的、优先级更高的任务。它们通常用于在当前宏任务结束之后,但在下一个宏任务开始之前,执行一些需要即时处理的工作。一个事件循环迭代中,微任务队列会完全清空。

常见的微任务包括:

  • Promise.then(), Promise.catch(), Promise.finally() 的回调。
  • async/await 中的 await 之后的代码(本质上也是 Promise)。
  • MutationObserver 的回调。
  • queueMicrotask() API。

4.3. 事件循环的详细执行步骤

现在,我们把所有组件和任务类型整合起来,详细描述事件循环的执行流程:

  1. 执行初始同步代码: JavaScript引擎开始执行全局同步代码。这些代码被推入调用栈,按顺序执行。
  2. 遇到异步任务:
    • 如果遇到宏任务API(如 setTimeout),它会将回调函数注册到Web APIs模块,并在相应事件发生后将其放入宏任务队列。
    • 如果遇到微任务API(如 Promise.then),它会将回调函数注册到Web APIs模块,并在相应事件发生后将其放入微任务队列。
  3. 调用栈清空: 当所有初始同步代码执行完毕,调用栈变空。
  4. 事件循环开始: 此时,事件循环开始其主要工作:
    a. 检查微任务队列: 事件循环会立即检查微任务队列。如果微任务队列不为空,它会取出队列中的所有微任务,并将它们依次推入调用栈执行,直到微任务队列完全清空。
    b. 渲染(浏览器特有): 在浏览器环境中,清空微任务队列后,浏览器可能会执行一次渲染操作,更新UI。这通常发生在下一个宏任务被选取之前。
    c. 检查宏任务队列: 如果微任务队列已空(或从未有微任务),事件循环会从宏任务队列中取出 一个 任务(FIFO),并将其推入调用栈执行。
    d. 重复: 当这个宏任务执行完毕,调用栈再次变空时,事件循环会回到步骤4a,再次检查微任务队列,然后是下一个宏任务,如此循环往复,永不停歇。

总结表格:宏任务 vs. 微任务

特性 宏任务(Macrotask / Task) 微任务(Microtask)
优先级 低于微任务,每次事件循环迭代只处理一个 高于宏任务,每次事件循环迭代会清空所有微任务
来源 setTimeout, setInterval, I/O, UI事件, script 整体 Promise.then/catch/finally, async/await, MutationObserver, queueMicrotask
时机 在当前微任务队列清空后,从宏任务队列中取一个执行 在当前宏任务执行后,下一个宏任务执行前,清空所有微任务
影响 可能导致UI卡顿(如果回调耗时过长) 通常不会直接导致UI卡顿,因为它在当前渲染帧内处理
例子 点击事件回调,网络请求回调,定时器回调 Promise链中的下一步,DOM变化观察者回调

5. 代码示例:追踪执行顺序

理论知识固然重要,但通过实际代码来追踪执行顺序才能真正巩固理解。

示例 1:宏任务的基本执行

console.log('1. Start script');

setTimeout(() => {
    console.log('3. setTimeout callback');
}, 0); // 尽管是0ms,但它仍然是一个宏任务

console.log('2. End script');

执行顺序分析:

  1. console.log('1. Start script'):同步代码,立即执行。输出 1. Start script
  2. setTimeout(() => { ... }, 0):这是一个Web API调用。它的回调函数被注册,并在0ms后(实际上是尽快)被放入宏任务队列。JavaScript引擎继续执行同步代码。
  3. console.log('2. End script'):同步代码,立即执行。输出 2. End script
  4. 此时,所有同步代码执行完毕,调用栈为空。事件循环开始工作。
  5. 事件循环检查微任务队列,发现为空。
  6. 事件循环检查宏任务队列,发现其中有 setTimeout 的回调。将其取出,推入调用栈。
  7. setTimeout 的回调函数执行 console.log('3. setTimeout callback')。输出 3. setTimeout callback
  8. 回调执行完毕,调用栈再次为空。事件循环再次检查,发现所有队列都已为空,程序结束。

输出:

1. Start script
2. End script
3. setTimeout callback

示例 2:微任务的优先级

console.log('1. Start script');

setTimeout(() => {
    console.log('4. setTimeout callback (Macrotask)');
}, 0);

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

console.log('2. End script');

执行顺序分析:

  1. console.log('1. Start script'):同步执行。输出 1. Start script
  2. setTimeout(() => { ... }, 0):回调被注册,放入宏任务队列。
  3. Promise.resolve().then(() => { ... })Promise.resolve() 会立即创建一个已解决的 Promise。它的 .then() 回调被注册,并立即放入微任务队列。
  4. console.log('2. End script'):同步执行。输出 2. End script
  5. 所有同步代码执行完毕,调用栈为空。
  6. 事件循环开始。首先检查微任务队列,发现其中有 Promise.then 的回调。
  7. Promise.then 的回调推入调用栈执行。输出 3. Promise.then callback (Microtask)
  8. Promise.then 回调执行完毕,调用栈为空。微任务队列此时已清空。
  9. 事件循环检查宏任务队列,发现其中有 setTimeout 的回调。将其取出,推入调用栈。
  10. setTimeout 的回调执行。输出 4. setTimeout callback (Macrotask)
  11. 回调执行完毕,调用栈为空。所有队列都已为空,程序结束。

输出:

1. Start script
2. End script
3. Promise.then callback (Microtask)
4. setTimeout callback (Macrotask)

这个例子清晰地展示了微任务在宏任务之前执行的优先级。

示例 3:复杂的宏任务与微任务交错

console.log('A');

setTimeout(() => {
    console.log('B');
    Promise.resolve().then(() => console.log('C'));
}, 0);

Promise.resolve().then(() => {
    console.log('D');
    setTimeout(() => console.log('E'), 0);
});

console.log('F');

执行顺序分析:

  1. 同步阶段:

    • console.log('A'):同步执行,输出 A
    • setTimeout(() => { console.log('B'); Promise.resolve().then(() => console.log('C')); }, 0):回调函数 cb1 被放入宏任务队列。
    • Promise.resolve().then(() => { console.log('D'); setTimeout(() => console.log('E'), 0); })Promise.resolve() 立即解决,其 .then() 回调函数 cb2 被放入微任务队列。
    • console.log('F'):同步执行,输出 F
    • 此时,调用栈为空。
  2. 第一次事件循环迭代:

    • 清空微任务队列: 事件循环发现微任务队列中有 cb2
      • cb2 被推入调用栈执行。
      • console.log('D'):输出 D
      • setTimeout(() => console.log('E'), 0):回调函数 cb3 被放入宏任务队列。
      • cb2 执行完毕,从调用栈弹出。微任务队列清空。
    • 处理一个宏任务: 事件循环发现宏任务队列中有 cb1
      • cb1 被推入调用栈执行。
      • console.log('B'):输出 B
      • Promise.resolve().then(() => console.log('C'))Promise.resolve() 立即解决,其 .then() 回调函数 cb4 被放入微任务队列。
      • cb1 执行完毕,从调用栈弹出。
  3. 第二次事件循环迭代:

    • 清空微任务队列: 事件循环发现微任务队列中有 cb4
      • cb4 被推入调用栈执行。
      • console.log('C'):输出 C
      • cb4 执行完毕,从调用栈弹出。微任务队列清空。
    • 处理一个宏任务: 事件循环发现宏任务队列中有 cb3
      • cb3 被推入调用栈执行。
      • console.log('E'):输出 E
      • cb3 执行完毕,从调用栈弹出。
  4. 所有队列清空,程序结束。

输出:

A
F
D
B
C
E

这个例子完美地展示了同步代码优先,然后是微任务清空,再然后是单个宏任务,接着再清空微任务,循环往复。

示例 4:async/await 与事件循环

async/await 是 ES2017 引入的语法糖,它使得异步代码看起来像同步代码一样。但其底层仍然是 Promise 和事件循环。

await 一个 Promise 时:

  • 如果 Promise 已经解决(resolved),那么 await 表达式会立即求值,函数会继续同步执行。
  • 如果 Promise 尚未解决(pending),那么 async 函数会暂停执行,并将剩余的代码包装成一个微任务,等待 Promise 解决。一旦 Promise 解决,这个微任务就会被放入微任务队列,等待执行。
async function asyncFunc() {
    console.log('A. Inside asyncFunc - before await');
    await Promise.resolve(); // 立即解决的Promise
    console.log('C. Inside asyncFunc - after await');
}

console.log('1. Global Start');
asyncFunc();
Promise.resolve().then(() => console.log('B. Promise then (Microtask)'));
console.log('2. Global End');

执行顺序分析:

  1. 同步阶段:

    • console.log('1. Global Start'):同步执行,输出 1. Global Start
    • asyncFunc() 被调用。
      • asyncFunc 内部:console.log('A. Inside asyncFunc - before await'):同步执行,输出 A. Inside asyncFunc - before await
      • await Promise.resolve()Promise.resolve() 立即解决。因为 await 后面的是一个已解决的 Promise,所以 asyncFunc 的剩余部分 (console.log('C. Inside asyncFunc - after await')) 会被包装成一个微任务,并立即放入微任务队列。
      • asyncFunc 的同步部分执行完毕。
    • Promise.resolve().then(() => console.log('B. Promise then (Microtask)')):此 then 回调也被放入微任务队列。
    • console.log('2. Global End'):同步执行,输出 2. Global End
    • 此时,调用栈为空。微任务队列中现在有两个任务:asyncFuncawait 之后的部分,和 Promise.resolve().then 的回调。
  2. 第一次事件循环迭代:

    • 清空微任务队列: 事件循环发现微任务队列不为空。
      • 首先,取出 asyncFuncawait 之后的部分(因为它是先被推入队列的)。推入调用栈执行。
      • console.log('C. Inside asyncFunc - after await'):输出 C. Inside asyncFunc - after await
      • 该微任务执行完毕,从调用栈弹出。
      • 接着,取出 Promise.resolve().then 的回调。推入调用栈执行。
      • console.log('B. Promise then (Microtask)'):输出 B. Promise then (Microtask)
      • 该微任务执行完毕,从调用栈弹出。
      • 微任务队列清空。
    • 处理宏任务: 宏任务队列为空。
  3. 所有队列清空,程序结束。

输出:

1. Global Start
A. Inside asyncFunc - before await
2. Global End
C. Inside asyncFunc - after await
B. Promise then (Microtask)

这个例子展示了 await Promise.resolve() 实际上是将 await 后的代码放入微任务队列,因此它会比后续的同步代码晚执行,但比下一个宏任务早执行。

6. 事件循环与UI渲染

在浏览器环境中,事件循环不仅仅管理JavaScript代码的执行,它还与浏览器的渲染机制紧密相关。

通常,UI渲染会发生在每个宏任务执行完毕之后,下一个宏任务开始之前。这意味着,如果在一次事件循环迭代中,一个宏任务(例如一个 setTimeout 回调)执行了很长时间,它将阻塞UI的更新,导致页面卡顿。

例如:

const btn = document.getElementById('myButton');
btn.addEventListener('click', () => {
    console.log('Button clicked');
    // 模拟一个非常耗时的同步操作
    let sum = 0;
    for (let i = 0; i < 1000000000; i++) {
        sum += i;
    }
    console.log('Heavy computation finished:', sum);
    // 此时UI会被阻塞,直到上面的循环完成
});

console.log('Script loaded');

当你点击按钮时,click 事件的回调(一个宏任务)会被推入宏任务队列。当事件循环取出并执行这个回调时,耗时的 for 循环会完全阻塞JavaScript引擎。在这段时间内,浏览器无法响应其他用户输入,也无法进行UI渲染。页面会显得“冻结”。

为了避免这种情况,对于耗时的计算,我们通常会考虑:

  • 分片处理: 将大任务拆分成小任务,通过 setTimeout(..., 0)requestIdleCallback 在不同的事件循环迭代中执行,给浏览器留出渲染和响应用户输入的时间。
  • Web Workers: 将计算密集型任务完全转移到独立的线程中执行,不占用主线程。

requestAnimationFrame:优化动画和渲染

requestAnimationFrame (rAF) 是一个专门用于动画的Web API。它的回调函数会在浏览器下一次重绘之前执行。浏览器通常以每秒60帧(60fps)的频率进行重绘,所以 rAF 的回调会在大约16.7ms的间隔内触发。

rAF 的特点:

  • 与浏览器渲染同步: 确保动画流畅,避免抖动。
  • 在浏览器非活动标签页时暂停: 节省资源。
  • 优先级: 它的回调通常被视为一个特殊的宏任务,在一次渲染循环中,所有 rAF 回调会在DOM更新和样式计算之后,但在实际绘制之前执行。
function animate() {
    // 执行动画逻辑,更新DOM
    console.log('Animating...');
    requestAnimationFrame(animate); // 请求下一帧
}

// requestAnimationFrame(animate); // 启动动画

通过 rAF 调度动画,可以确保你的DOM操作在浏览器最合适的时机进行,从而获得最佳的视觉流畅度。

7. Node.js 环境下的事件循环(简要提及)

虽然本文主要聚焦于浏览器环境下的事件循环,但Node.js也有其自己的事件循环实现,尽管核心概念相似,但在某些细节上有所不同,特别是任务队列的划分和阶段。

Node.js 的事件循环分为几个阶段,每个阶段都有其自己的任务队列:

  • timers (定时器): 执行 setTimeoutsetInterval 的回调。
  • pending callbacks (待定回调): 执行某些系统操作的回调。
  • idle, prepare (空闲, 准备): 仅在内部使用。
  • poll (轮询):
    • 检索新的I/O事件(如网络请求完成)。
    • 执行几乎所有的回调(除了定时器和 close 回调)。
    • 如果队列为空,Node.js可能会在此阶段阻塞,等待新事件,或者进入 check 阶段。
  • check (检查): 执行 setImmediate 的回调。
  • close callbacks (关闭回调): 执行 close 事件的回调,例如 socket.on('close', ...)

Node.js 特有的微任务:process.nextTick()

Node.js 中有一个特殊的微任务函数 process.nextTick()。它的优先级甚至高于标准的微任务队列。process.nextTick() 的回调会在当前操作完成之后,但在事件循环的任何阶段开始之前执行。

这意味着 process.nextTick() 的回调会比 Promise.then() 更早执行。

console.log('1. Script start');

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

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

process.nextTick(() => {
    console.log('2. process.nextTick callback');
});

console.log('5. Script end');

在 Node.js 环境中,输出会是:

1. Script start
5. Script end
2. process.nextTick callback
3. Promise.then callback
4. setTimeout callback

这个例子突出了 process.nextTick() 的极高优先级,它在当前同步代码执行完毕后立即执行,甚至在其他微任务之前。

8. 常见陷阱与最佳实践

理解事件循环不仅仅是为了炫耀知识,更是为了避免实际开发中的错误和性能问题。

8.1. 避免阻塞主线程

  • 陷阱: 在主线程中执行长时间运行的同步计算,导致UI冻结。
  • 最佳实践:
    • 将计算密集型任务分解成小块,使用 setTimeout(..., 0)requestIdleCallback 分批处理。
    • 利用 Web Workers 将计算转移到后台线程。

8.2. 正确处理 Promise 链

  • 陷阱: 误认为 Promise.then() 会立即执行,或者不理解其微任务特性。
  • 最佳实践: 记住 then, catch, finally 的回调都是微任务,它们会在当前宏任务结束且所有同步代码执行完毕后,但在下一个宏任务开始前执行。这对于控制异步流程至关重要。

8.3. 理解 setTimeout(..., 0) 的含义

  • 陷阱: 认为 setTimeout(func, 0) 会立即执行 func
  • 最佳实践: setTimeout(func, 0) 只是将 func 放入宏任务队列,等待当前调用栈清空且所有微任务执行完毕后,才能在下一个事件循环迭代中被取出执行。它并非“立即执行”,而是“尽可能快地在下一个宏任务时机执行”。

8.4. Debouncing 和 Throttling

  • 应用: 这两种优化技术都依赖于 setTimeout 来控制事件触发频率。
  • Debouncing (防抖): 在事件停止触发一段时间后才执行回调。例如,搜索框输入,用户停止输入后才发起搜索请求。
  • Throttling (节流): 在一段时间内只执行一次回调。例如,窗口resize或滚动事件,每隔固定时间才响应一次。

8.5. 警惕无限微任务循环

  • 陷阱: 在一个微任务中不断地创建新的微任务,理论上可能导致宏任务永远无法执行(微任务饥饿)。
  • 示例:
     function createInfiniteMicrotasks() {
         Promise.resolve().then(() => {
             console.log('Microtask running');
             createInfiniteMicrotasks(); // 递归调用,不断添加新的微任务
         });
     }
     // createInfiniteMicrotasks(); // 不要轻易尝试,可能导致浏览器卡死
     // setTimeout(() => console.log('This might never run'), 0);
  • 最佳实践: 确保你的微任务链是有限的,或者最终会通过某种机制终止。

9. 掌握事件循环:迈向更专业的异步编程

事件循环是JavaScript并发模型的基础。它并非一个独立的功能,而是由JavaScript引擎、Web APIs、各种任务队列以及一个协调器(事件循环本身)共同构成的精巧系统。

通过从执行顺序入手,我们详细了解了:

  • JavaScript的单线程特性和调用栈。
  • Web APIs如何将异步任务从主线程中剥离。
  • 宏任务和微任务的本质区别及其在事件循环中的优先级。
  • 事件循环如何协调这些组件,确保代码的非阻塞执行。

掌握事件循环,你将不再盲目地编写异步代码。你能够准确预测代码的执行时机,避免常见的性能陷阱,更自信地调试复杂的异步流程。这将是你从JavaScript初学者迈向资深开发者的关键一步。未来的前端和后端开发,异步编程无处不在,深入理解其核心机制,无疑会让你在构建高性能、高响应度应用时如鱼得水。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注