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