【JavaScript】详解事件循环机制(Event Loop)

266 阅读4分钟

一、前言

JavaScript是一门单线程、非阻塞的脚本语言。

单线程指的是JavaScript同时只能执行一个任务,其余都必须在后面排队等待。注意事实上,JavaScript引擎有多个线程,单个脚本只能在一个线程上运行,其他线程都是在后台配合。

非阻塞指的是代码需要进行异步任务时(无法立刻返回结果,需要花一定时间才能返回的任务),主线程会将这个异步任务挂起,在异步任务返回结果时在根据一定规则去执行相应的回调。

那JavaScript是如何实现在“单线程”的情况下“非阻塞”的呢?这就是我们要在这篇文章了解的——事件循环(Event Loop)

二、宏任务与微任务

JS任务可以分成两类:同步任务和异步任务。同步任务,就是立即执行的任务,一般会直接进入到主线程中执行;异步任务,就是异步执行的任务,会通过事件队列(Event Queue)的机制来进行协调。

其实,异步任务也有不同。JavaScript之所以要区分微任务和宏任务,是为了保证异步操作的正确性和性能。

异步任务可以分成两类:微任务(micro task)和宏任务(macro task)。微任务是指在当前任务执行结束后立即执行的任务,宏任务是需要排队等待JavaScript引擎空闲时才能执行的任务。

属于宏任务的事件有:setInterval()setTimeout();属于微任务的事件有:Promise Async/Await

三、执行栈与事件队列

执行栈(Call Stack)

当我们调用方法时,JS会生成方法对应的执行上下文并加入到执行栈中。加入到执行栈后再执行函数,当函数执行完后出栈,继续执行下一个。

事件队列(Event Queue)

又名任务队列。不同的宿主环境有不同的任务队列,大多数宿主环境会把事件队列分成 宏任务微任务

四、事件循环的执行顺序

  • JavaScript代码的执行顺序是从上到下一行一行执行。
    • 如果某一行执行出错,则停止执行下面的代码。
    • 先执行同步代码再执行异步代码。
  • 同步代码在执行栈中执行,执行后直接出栈
  • 异步代码放到Web API中,等待合适时机,再加入事件队列中,待到执行栈为空时Event Loop会从事件队列中提取事件,推入执行栈中执行
    • 在异步代码中,微任务是比宏任务先执行的。JS引擎会先执行完所有微任务,再执行宏任务的第一个任务,直至宏任务队列为空。
    • 微任务在DOM渲染之前执行,宏任务在DOM渲染之后执行 事件循环机制图示.png

五、经典案例分析

写出以下程序的执行结果

console.log('1')
setTimeout(function callback() {
    console.log('2')
}, 1000)
new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4')
})
console.log('5')

正确答案为:

// 1
// 3
// 5
// 4
// 2

分析如下:

  1. JS先按从上到下的顺序执行代码,故console.log('1')代码进入执行栈中执行;
  2. 随后执行setTimeout(),定时器加入执行栈中,定时器中声明了一个回调函数callback()(之后称1号回调)。1号回调为异步任务,即进入Web API,在1s后被推入宏任务队列中,最后定时器出栈;
  3. 接下来new Promise()进入执行栈,声明定义又一个回调函数(之后称2号回调),2号回调入栈;then()进入Web API;
  4. 执行2号回调时,第一行代码console.log('3')入栈执行,执行完后出栈;
  5. 接下来执行2号回调的第二行代码,此时Promise状态切换至 fulfilled ,then()被推入微任务队列;
  6. 最后console.log('5')代码进入执行栈执行,执行完后出栈;
  7. 此时执行栈为空,Event Loop开始查看微任务队列是否有任务,微任务队列中存在then()任务,故先执行,随后res => {}入栈开始执行,接着console.log('4')入栈执行,执行完后依次出栈;
  8. 执行完then()后微任务队列为空,随后Event Loop查看宏任务队列是否为空,宏任务队列中有1号回调,故1号回调入栈,接着console.log('2')也入栈,执行完后依次出栈;
  9. 执行结束。 经典案例图解分析.webp