面试高频:事件循环与 async 语法糖
前言
在当今众多的编程语言里,存在着单线程和多线程之分。单线程意味着在特定的时间点,仅能专注于执行一项任务。相较而言,多线程则能够在相同的时间区间内同时处理多个任务。JavaScript 便是典型的单线程编程语言。JavaScript 是单线程运行的,但它能够处理异步操作,这主要归功于事件循环(Event Loop)。
事件循环
Event Loop是 JavaScript 运行时环境中的一个关键机制,用于协调和管理代码的执行顺序,尤其是处理同步和异步任务。它的主要工作是确保 JavaScript 能够在单线程的环境中高效地处理各种任务,而不会因为某些耗时的操作而导致整个程序的阻塞。
同步/异步
同步和异步就是两种不同的执行任务的方式。
同步是按照代码的书写顺序执行,不管要执行的代码耗时有多大,都得等它执行完才能执行后续的代码,后续的代码都处于阻塞的状态。常见的同步任务包括基本的赋值声明操作、算术运算、条件判断、循环结构、函数的调用等。
异步也是按照代码的书写顺序执行的,但是与同步不同的是,在遇到需要等待长时间才能执行完的代码时,会将其阻塞掉,并且放入阻塞队列中,然后按顺序执行不耗时的代码,在不耗时的代码执行完后,再依次将阻塞的代码从阻塞队列中取出来执行。
常见的异步任务有:
setTimeout
:设置一个定时器,在经过指定时间后再执行回调函数。setInterval
:按照指定的时间间隔重复执行回调函数。Ajax
请求:从服务器获取数据。
eg:
let a = 1
console.log(a);
setTimeout(() => {
a++
}, 1000)
console.log(a);
function bar() {
console.log('bar');
}
bar()
按照同步的代码执行方式的输出结果是:1 2 bar
,按照异步的代码执行方式的输出结果是:1 1 bar
。这段JavaScript代码的实际的结果是1 1 bar
,这就说明了JavaScript代码是异步执行的。这样非常合理,如果是同步执行代码,这样会大大降低效率。
微任务/宏任务
异步任务又可以微任务和宏任务。
- 微任务:微任务是相对较小且执行迅速的异步任务。常见的微任务有:
promise.then()
,process.nextTick()
,MutationObserver()
等。 - 宏任务:宏任务是异步任务中规模大并且耗时的任务。常见的宏任务有
script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI-rendering
等。
微任务的优先级高于宏任务,只有在微任务执行完后才会执行宏任务。然而宏任务是事件循环的基本单位,每一次事件循环处理一个宏任务。
因此事件循环的工作组件包含有微任务队列和宏任务队列。
事件循环步骤
- 在主线程上依次执行所有同步任务,形成一个执行栈。
- 存在任务阻塞队列,用于存放除所有同步任务外的异步任务。异步任务中的宏任务和微任务会分别被放入到宏任务队列和微任务队列中。
- 当主线程的执行栈为空时,系统会检查微任务队列是否为空。
- 若微任务队列不为空,则取出一个微任务并将其执行;然后继续检查微任务队列,重复此过程,直到微任务队列为空。
- 若微任务队列为空,则取出宏任务队列中的一个宏任务并将其执行。
- 执行完一个宏任务后,会再次检查微任务队列是否为空,若有新的微任务则继续执行微任务,如此循环往复。
执行一个宏任务就是开启下一次事件循环,因此宏任务是事件循环的基本单位。
简单来说就是执行同步任务时,将微任务和宏任务推进各自的阻塞队列中,在同步任务执行完成后再执行微任务,再微任务执行完成后执行宏任务,然而一个宏任务里面可能还存在着同步任务、微任务和宏任务,这样就有开启了一次循环。
实战
通过事件循环的执行步骤得出以下代码的输出结果。
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
}).then(() => {
console.log(3);
setTimeout(() => { //setTimeout2
console.log(4);
}, 0)
})
setTimeout(() => { //setTimeout1
console.log(5);
setTimeout(() => { //setTimeout3
console.log(6);
}, 0)
}, 0)
console.log(7);
-
同步任务:执行
console.log(1)
。微任务队列: 宏任务队列: 现在输出:1
-
同步任务:执行
console.log(2)
,因为因为Promise
构造函数内的执行器函数会同步执行。微任务队列: 宏任务队列: 现在输出:1 2
-
将
promise.then()
挂到微任务队列里。微任务队列:promise.then() 宏任务队列: 现在输出:1 2
-
将
setTimeout
1挂到宏任务队列中。微任务队列:promise.then() 宏任务队列:setTimeout1 现在输出:1 2
-
同步任务:执行
console.log(7)
。微任务队列:promise.then() 宏任务队列:setTimeout1 现在输出:1 2 7
-
同步任务执行完后,在微任务队列里执行微任务,执行
promise.then()
。微任务队列: 宏任务队列:setTimeout1 现在输出:1 2 7
-
微任务:执行
console.log(3)
。微任务队列: 宏任务队列:setTimeout1 现在输出:1 2 7 3
-
将宏任务
setTimeout
2挂到宏任务队列中。微任务队列: 宏任务队列:setTimeout2 setTimeout1 现在输出:1 2 7 3
-
微任务执行完毕后,执行宏任务队列里的宏任务。开启下一次事件循环,从同步任务到微任务再到宏任务。
微任务队列: 宏任务队列:setTimeout2 现在输出:1 2 7 3
-
同步任务:执行
console.log(5)
。微任务队列: 宏任务队列:setTimeout2 现在输出:1 2 7 3 5
-
将
setTimeout3
挂到宏任务队列中。微任务队列: 宏任务队列:setTimeout3 setTimeout2 现在输出:1 2 7 3 5
-
此时已经执行完一个宏任务了,然而微任务队列为空,则进行执行宏任务队列里的宏任务。
微任务队列: 宏任务队列:setTimeout3 现在输出:1 2 7 3 5
-
同步任务:执行
console.log(4)
。微任务队列: 宏任务队列:setTimeout3 现在输出:1 2 7 3 5 4
-
微任务还是为空,则进行执行宏任务。
微任务队列: 宏任务队列: 现在输出:1 2 7 3 5 4
-
同步任务:执行
console.log(6)
,结束。微任务队列: 宏任务队列: 现在输出:1 2 7 3 5 4 6
async/await
在项目开发过程中总会出现一个不耗时的任务需要等待一个耗时任务的结果,所以该不耗时的任务就需要紧跟在那个耗时任务的后面执行,这样才不会出错。
eg:想要最终获取到的data
有值。
let data = null;
function getData() {
setTimeout(() => {
data = [1, 2, 3]
}, 1000)
}
function another() {
console.log(data);
}
getData()
another()
执行这样的代码后,最终得出来的结果会是null
。
那就需要通过Promise
进行处理。
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data = [1, 2, 3]
resolve()
}, 1000)
})
}
function another() {
console.log(data);
}
getData().then(another);
当需要按顺序执行多个异步操作时,如果使用 Promise.then()
链,代码可能会变得冗长且难以阅读与维护。
async/await
语法糖的出现有效地改善了这种状况。通过使用 async
函数和 await
关键字,可以采用更类似于同步代码的结构来编写异步操作,从而让代码更具可读性和可维护性。
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
data = [1, 2, 3]
resolve()
}, 1000)
})
}
function another() {
console.log(data);
}
async function main() {
await getData()
another()
}
其中有一些需要注意的东西:
- 当在
async
函数中使用await
时,它所等待的表达式必须返回一个Promise
对象。如果表达式不是Promise
,则会自动将其包装为已解决(resolved)的Promise
并立即获取其值。 - 函数中
await
后面跟着的代码都会被阻塞,然后被放入微队列中,因为它们相当于是Promise.then()
的代码。 async
相当于在函数里返回了一个Promise
。
测试
以下代码的输出结果是什么呢?
console.log('script start');
async function async1() {
await async2()
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');
按照事件循环的执行步骤一步步推就能得出正确的输出结果了。如果你答对了就说明你已经拿下来事件循环了,如果没有答对就多看几遍哦。
结果是:script start 、async2 end、promise、script end、async1 end、then1、then2、setTimeout
。
小结
事件循环是面试常问的类型,掌握它也很简单,只需要牢记事件循环的执行步骤就好了。
转载自:https://juejin.cn/post/7389083163333476379