深入理解 JavaScript 事件循环机制

83 阅读5分钟

JavaScript 的事件循环机制决定了代码的执行顺序,是理解异步编程的核心。为了帮助大家更全面地掌握事件循环,本篇文章将通过一个综合案例深入解析事件循环中同步任务、异步任务、宏任务与微任务的执行顺序。

1. JavaScript 事件循环机制概述

js是单线程的,一次只能执行一个任务,为了避免阻塞主线程,js通过事件循环机制来处理同步和异步代码。

  • 同步任务:立即执行的任务,同步任务会直接进入到主线程中执行;
  • 异步任务:异步任务进入任务队列等待主线程内的任任务执行完毕后,会去任务队列读取对应的任务,推入主线程执行,比如ajax网络请求,setTimeout定时函数等

宏任务与微任务:异步任务可以分为宏任务和微任务,每次执行完一个宏任务之后,都会立即执行所有的微任务,然后再执行下一个宏任务,这样循环执行任务代码,直到循环完所有代码。

宏任务:整体代码块(如 script 标签中的代码)、setTimeoutsetInterval

微任务:包括 Promise 的回调函数、process.nextTick(Node.js)等

2. 综合案例:事件循环中的执行顺序

我们将通过一个综合案例来解释事件循环的执行顺序和宏任务、微任务的优先级。


console.log("script start");

setTimeout(() => {
  console.log("setTimeout 1");
}, 0);

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

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

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2");
}

async1();

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

console.log("script end");

2.1 执行顺序分析

  1. 主线程执行同步代码

    • console.log("script start"):立即执行,输出 script start
    • setTimeout(..., 0):定时器回调被放入宏任务队列,稍后执行。
    • Promise.resolve().then(...)then 回调被放入微任务队列。
    • setTimeout(..., 0):另一个定时器回调被放入宏任务队列。
    • async1():调用 async1 函数,输出 async1 start,然后遇到 await,暂停执行后续代码,继续执行 async2,输出 async2
    • new Promise(...):立即执行构造函数,输出 promise 3then 回调被放入微任务队列。
    • console.log("script end"):立即执行,输出 script end
  2. 执行微任务队列

    • 执行第一个微任务队列中的 promise 1,输出 promise 1
    • 执行第二个微任务队列中的 promise 2,输出 promise 2
    • 执行 async1 函数中 await 之后的部分,输出 async1 end
    • 执行 new Promisethen 回调,输出 promise 4
  3. 执行宏任务队列

    • 执行第一个 setTimeout 的回调,输出 setTimeout 1
    • 执行第二个 setTimeout 的回调,输出 setTimeout 2

2.2 综合案例的执行结果

最终输出顺序:

arduino
复制代码
script start
async1 start
async2
promise 3
script end
promise 1
promise 2
async1 end
promise 4
setTimeout 1
setTimeout 2

2.3 详细执行流程解析

  1. 同步代码执行阶段

    • script start 作为同步任务立即输出。
    • setTimeout 被放入宏任务队列,稍后执行。
    • Promise.resolve().then(...)then 回调被放入微任务队列。
    • async1() 调用时,async1 start 被立即输出,async2 被调用,输出 async2await 后面的代码被放入微任务队列。
    • 新的 Promise 实例被创建,立即输出 promise 3then 回调被放入微任务队列。
    • 最后输出 script end
  2. 微任务队列执行阶段

    • 执行微任务队列中的 promise 1,输出 promise 1
    • 紧接着执行 promise 2,输出 promise 2
    • 执行 async1await 之后的 console.log("async1 end"),输出 async1 end
    • 执行 new Promisethen 回调,输出 promise 4
  3. 宏任务队列执行阶段

    • 执行宏任务队列中的第一个 setTimeout 回调,输出 setTimeout 1
    • 执行宏任务队列中的第二个 setTimeout 回调,输出 setTimeout 2

3. 深入理解 async/await 与事件循环

async/awaitPromise 的语法糖,它会在 await 语句后将后续代码放入微任务队列中。

3.1 案例:async/await 与事件循环的交互

javascript
复制代码
async function foo() {
  console.log('foo start');
  await bar();
  console.log('foo end');
}

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

console.log('script start');
foo();
console.log('script end');

执行顺序:

  1. console.log('script start'):输出 script start
  2. 调用 foo(),输出 foo start,然后 await bar(),调用 bar(),输出 barfoo 函数暂停,await 之后的代码被放入微任务队列中。
  3. console.log('script end'):输出 script end
  4. 主线程空闲,开始执行微任务队列,输出 foo end

最终输出:

sql
复制代码
script start
foo start
bar
script end
foo end

4. 常见事件循环陷阱

4.1 setTimeout 延迟的误解

javascript
复制代码
setTimeout(() => {
  console.log('setTimeout 1');
}, 0);

for (let i = 0; i < 1000000000; i++) {} // 模拟长时间的同步操作

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

输出顺序:

arduino
复制代码
setTimeout 1
setTimeout 2

解释:尽管两个 setTimeout 都设置了 0ms 延迟,但由于主线程被长时间同步任务阻塞,定时器的回调会被延迟执行。

4.2 Promise 链的递归执行

javascript
复制代码
Promise.resolve().then(() => {
  console.log("Promise 1");
  Promise.resolve().then(() => {
    console.log("Promise 2");
  });
});

输出顺序:

javascript
复制代码
Promise 1
Promise 2

解释:Promise 1 的回调进入微任务队列并被执行,在 Promise 1 回调中创建的 Promise 2 的回调会在当前微任务队列执行完毕后执行。

5. 性能优化建议

  1. 避免主线程阻塞:将复杂的计算或大数据处理放在 Web WorkerNode.jsWorker Threads 中处理。
  2. 合理使用微任务与宏任务:通过微任务处理优先级较高的任务,如使用 PromisequeueMicrotask
  3. 分解复杂的异步任务:将复杂的异步任务分解成多个小任务,避免长时间阻塞主线程。

6. 结语

通过本文的综合案例解析,我们深入了解了 JavaScript 事件循环机制的执行顺序、宏任务与微任务的区别,以及 async/await 与事件循环的交互原理。理解事件循环机制能够帮助我们更好地编写高效、性能优良的 JavaScript 代码。希望本文能够为你在学习和工作中提供有益的参考!