理解 JavaScript 事件循环机制:从同步到异步的深度解析

620 阅读8分钟

引言

JavaScript是一门广泛应用于前端开发的单线程编程语言,意味着在同一时间内只能执行一个任务。为了高效处理异步操作,JavaScript引入了事件循环机制。事件循环是JavaScript引擎用来管理同步与异步任务执行的核心机制,确保任务按照正确的顺序执行,从而保证应用的流畅运行。

那么理解事件循环机制对于JavaScript的学习至关重要。呆同学将带大家解析这一机制,帮助大家更好地理解JavaScript的执行过程,提升编写高性能应用的能力。

JavaScript中的同步与异步代码

JavaScript中的代码可以分为同步代码和异步代码,这两者在执行顺序和处理方式上存在显著差异。

同步代码

同步代码是指按照代码书写的顺序依次执行的代码。每一行代码在当前行执行完成之前,下一行代码不会开始执行。同步代码不会立即返回,而是要等到当前任务完成后才返回结果,且在V8引擎眼中,这是不耗时执行的代码。例如:

console.log('同步代码开始'); // 1
let result = 1 + 2; // 2
console.log('计算结果:', result); // 3
console.log('同步代码结束'); // 4

在上述示例中,代码会按照书写顺序依次执行,从上到下没有任何中断。

异步代码

异步代码是指不会立即执行完毕,可能需要等待某些操作完成后再继续执行的代码。异步代码会将一些任务推迟到将来的某个时间点执行,而不会阻塞后续代码的运行,需要耗时执行的代码。常见的异步操作包括定时器、网络请求、文件读写等。例如:

console.log('异步代码开始'); // 1
setTimeout(() => {
    console.log('这是一个异步操作'); // 3
}, 1000);
console.log('异步代码结束'); // 2

在上述示例中,setTimeout是一个异步操作,它会在1秒后执行指定的函数,但不会阻塞后续代码的执行。因此,console.log('异步代码结束'); 会在 console.log('这是一个异步操作'); 之前执行。

事件循环的定义与作用

事件循环(Event Loop)是JavaScript引擎用来管理和协调同步任务与异步任务执行的核心机制。它的主要作用是确保所有任务按照正确的顺序执行,避免阻塞,并保持应用程序的流畅运行。事件循环通过将任务分为宏任务和微任务,并依次处理这些任务,来实现异步操作的高效管理。

事件循环的工作流程

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

  1. 执行同步代码(这属于是宏任务)
  2. 同步执行完毕后,检查是否有异步需要执行
  3. 执行所有的微任务
  4. 微任务执行完毕后,如果有需要就会渲染页面
  5. 执行异步宏任务,也是开启下一次事件循环

通过上述步骤,事件循环确保所有任务按照正确的顺序执行,实现同步与异步操作的高效管理。这种机制使得JavaScript在处理复杂的异步操作时,依然能保持流畅的用户体验。

宏任务与微任务

宏任务

宏任务是JavaScript引擎中较大的一类任务,它们会在主线程的执行上下文中排队,并在事件循环的每一轮中执行。每次事件循环都会处理一个宏任务。

宏任务包括:script, setTimeout(), setInterval(), setImmediate(), I/O, UI-rendering

例如:

console.log('宏任务开始'); // 1

setTimeout(() => {
    console.log('setTimeout 宏任务'); // 3
}, 0);

console.log('宏任务结束'); // 2

在上述示例中,console.log('宏任务开始');console.log('宏任务结束'); 属于同步代码,也是宏任务的一部分。而 setTimeout 设置的回调函数会在下一轮事件循环中执行,作为一个新的宏任务,属于异步代码。

微任务

微任务是较小且更轻量级的任务,它们会在当前宏任务执行完毕后立即执行,但在进入下一个宏任务之前。微任务的执行优先级高于下一个宏任务。

微任务包括:promise.then(), process.nextTick()(Node.js环境), MutationObserver()

例如:

console.log('宏任务开始'); // 1

Promise.resolve().then(() => {
    console.log('Promise 微任务'); // 3
});

console.log('宏任务结束'); // 2

在上述示例中,Promise.then() 创建的回调函数会在当前宏任务执行完毕后立即执行,作为一个微任务。

让我们通过具体的代码示例来更详细地说明宏任务与微任务的执行顺序。

console.log(1);

new Promise((resolve, reject) => {
    console.log(2);
    resolve();
})
.then(() => {
    console.log(3);
})
.then(() => {
    console.log(4);
});

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

console.log(6);
// 最终的输出顺序是:1 2 6 3 4 5

执行顺序解析:

  1. 执行同步代码

    console.log(1); 输出 1

    new Promise((resolve, reject) => { console.log(2); resolve(); }) 输出 2

    console.log(6); 输出 6

    这部分同步代码属于宏任务,在执行完这些代码后会进入下一步。

  2. 检查并执行异步微任务

    Promise.resolve().then(...) 的第一个 then 回调会立即执行,输出 3

    ② 接着执行链式 then 回调,输出 4

    这些微任务是在当前宏任务执行完毕后立即执行的。

  3. 执行异步宏任务

    setTimeout(() => { console.log(5); }, 0) 是一个宏任务,在微任务执行完毕后,会在下一轮事件循环中执行,输出 5

需要强调的是,上面代码中的new Promise并不是微任务,而是宏任务,属于同步代码;但之后的.then方法是微任务

这种执行顺序清晰地展示了事件循环中的宏任务和微任务的执行逻辑:同步代码(宏任务)先执行,之后执行所有的微任务,最后才执行异步宏任务。

在对以上js循环机制的学习后,我们加入async/await并通过一个例子来更好的了解和巩固一下:

console.log('script start');

async function async1() {
    await async2(); // await 会将后续的代码阻塞进微任务队列中 
    console.log('async1 end');
}

async function async2() {
    console.log('async2 end');
}

async1();

setTimeout(function () {
    console.log('setTimeout');
}, 0);

new Promise(function (resolve, reject) {
    console.log('promise');
    resolve();
})
.then(() => {
    console.log('then1');
})
.then(() => {
    console.log('then2');
});

console.log('script end');

步骤解析:从同步到异步,宏任务与微任务的执行顺序

  • 执行同步代码

    1. console.log('script start'); 输出 script start

    2. 定义了 async1async2 函数

    3. 调用 async1()

      ① 由于执行await async2(),故async2() 先被调用,console.log('async2 end'); 输出 async2 end

      ② 但因为await async2(); 使得后面的 console.log('async1 end'); 被推入微任务队列

    4. 创建 Promiseconsole.log('promise'); 输出 promise

    5. console.log('script end'); 输出 script end,同步代码执行完毕

  • 代码中的异步宏任务

    setTimeout(function () { console.log('setTimeout'); }, 0); 设置了一个异步宏任务

  • 代码中的微任务

    Promise.resolve().then(...) 设置了两个 then 回调,这些回调会被推入微任务队列,那么此时微任务队列中便有三个了,分别是console.log('async1 end');、两个then回调

  • 执行微任务

    1. 根据队列的先进先出原则,先执行 async1 中的 await async2() 后的微任务,输出 async1 end

    2. 执行第一个 then 回调,输出 then1

    3. 执行第二个 then 回调,输出 then2

  • 执行宏任务

    执行 setTimeout 的回调函数,输出 setTimeout

最后的输出结果也和我们分析的一样:

image.png

通过这个示例代码和详细解析,我们可以清楚地看到事件循环中同步任务、微任务和宏任务的执行顺序。这种执行顺序确保了JavaScript在单线程的情况下,能够高效处理异步操作,保持应用程序的流畅运行。

总结

我们通过一个表格对js的循环机制进行一个简单的总结

核心要点详细说明
事件循环(Event Loop)管理和协调同步任务与异步任务执行的核心机制
同步任务立即执行的任务,如函数调用、变量赋值等
异步任务包括微任务和宏任务,异步任务不会立即执行,需等待时机
宏任务(Macro Task)setTimeoutsetIntervalscript、I/O操作等
微任务(Micro Task)Promise.thenprocess.nextTick(Node.js)、MutationObserver
执行顺序先执行同步任务,然后执行所有微任务,最后执行一个宏任务,如此循环往复

我们在编写异步代码的时候,可以使用async/await,相比于回调函数和Promise链式调用,async/await提供了更简洁、更易读的语法来处理异步代码。通过掌握和合理应用事件循环机制,我们可以编写出高效、流畅、响应迅速的JavaScript代码,提升我们编程实力。

以上便是我对js中循环机制的一个分享,希望能够帮助到你,别忘了点赞收藏+关注哦~