前言
在 JavaScript 中,代码的执行顺序并不是完全按照它们的书写顺序,比如下面这段代码:
setTimeout(() => {
console.log(1)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(3)
}).then(res => {
console.log(res)
})
console.log(4)
如果按照书写顺序理解,输出顺序将会是:1 -> 2 -> 3 -> 4
。
但事实上并非如此,其最终的输出顺序其实是:2 -> 4 -> 3 -> 1
,跟书写顺序有很大的区别,想要理解为何如此,必须了解 JS 的事件循环机制。
什么是事件循环?
事件循环,即 Event Loops。用于协调事件、用户交互、JavaScript 脚本、DOM 渲染、网络请求等等的执行顺序问题。
ECMAScript 标准规定,一个遵循 ECMAScript 的客户端(浏览器、Node.js 等)必须遵循事件循环机制。
任务队列
事件循环是由一个个的任务队列组成的。任务队列(Task Queues)顾名思义就是一组任务的集合。
由于 JavaScript 是 单线程 语言,所以在 JS 中所有的任务都需要排队执行,这些任务共同组成了任务队列,依次排队执行的过程,形成一个执行栈(Execution Context Stack)。
在任务队列中最先执行是同步任务。
什么是同步任务?
同步任务(Synchronous Task),就是当上一个任务执行完成后,接下来可以立即执行的任务。它们在主线程上依次排队执行,直到清空。
比如,下面代码中的 for()
和 console.log()
将会依次执行,最终输出 start 0 1 2 end
。
console.log("start")
for (let i = 0; i < 3; i++) {
console.log(i)
}
console.log("end")
相对于同步任务,异步任务的执行充满了不确定性。
什么是异步任务?
异步任务(Asynchronous Task),就是需要等待被通知才以执行的任务。也就是说,它们不会直接进入主线程执行,而是进入到微任务队列或下一次事件循环中的任务队列进行等待。
常见的异步任务有:
XMLHttpRequest()
、fetch()
、WebSocket()
Promise.then()
、Promise.catch()
、Promise.finally()
setTimeout()
、setInterval()
等待,就意味着不确定性。比如,XMLHttpRequest()
等待服务器响应,Promise.then()
等待 resolve()
,setTimeout()
等待时间结束。
所以虽然都是异步任务,它们的执行的顺序仍然会有所区别。因此,我们将它们分为宏任务和微任务。
什么是宏任务?
宏任务(MacroTask),就是指进入任务队列的任务。比如:
setTimeout()
、setInterval()
document.appendChild()
setImmediate()
(Node.js 环境)<script />
由于当前任务队列已经处于执行状态,所以任务队列中遇到的宏任务将进入到下一次事件循环的任务队列,而微任务则会被放入到本次事件循环的微任务队列中。
什么是微任务?
微任务(Microtask 或 Jobs),每次事件循环都会有一个初始为空的微任务队列。常见的微任务有:
Promise.then()
、Promise.catch()
、Promise.finally()
MutationObserver()
(浏览器环境)process.nextTick()
(Node.js 环境)
测试题
看到这里,JavaScript 的事件循环机制差不多就解释完了,总的来说,其分为同步任务和异步任务,异步任务又分为宏任务和微任务。
大家可以通过下面的几道测试题检查一下自己的掌握程度。
-
一
setTimeout(() => { console.log(1) }, 0) for (let i = 2; i <= 3; i++) { console.log(i) } console.log(4) setTimeout(() => { console.log(5) }, 0) for (let i = 6; i <= 7; i++) { console.log(i) } console.log(8)
-
二
console.log(1) async function async1() { await async2() console.log(2) } async function async2() { console.log(3) } async1() setTimeout(() => { console.log(4) }, 0) new Promise(resolve => { console.log(5) resolve() }) .then(() => { console.log(6) }) .then(() => { console.log(7) }) console.log(8)
-
三
console.log(1) function a() { return new Promise(resolve => { console.log(2) setTimeout(() => { resolve() console.log(3) }, 0) }) } a().then(() => { console.log(4) })
参考资料: