介绍
亲爱的读者朋友们,今天我们将一起揭开JavaScript世界中一个至关重要的幕后英雄------事件循环(Event Loop)的神秘面纱。作为实现JavaScript异步编程的核心机制,事件循环对于理解和优化应用程序性能至关重要。无论你是初涉前端领域的开发者,还是寻求提升自身技术栈深度的老手,这篇文章都将助你全面掌握这一核心概念,并通过实际题目加深理解。
什么是事件循环?
在JavaScript中,由于其单线程执行模型,为了确保用户界面不会因长时间运行的脚本而阻塞,引入了事件循环机制。简单来说,事件循环就是一个持续循环的过程,它在执行栈为空时检查是否有待处理的异步任务,并将其依次放入执行栈中执行。
事件循环的基本流程:
- 执行全局脚本及同步代码块**。
- 当遇到异步任务(如setTimeout、Promise、I/O等)时,将相关回调函数放入任务队列(分为宏任务队列与微任务队列,w3c最新的解释已经取消了宏任务一说)。
- 执行栈为空时,事件循环从微任务队列中取出所有任务执行。
- 完成微任务队列后,事件循环从宏任务队列(其他队列)中取出一个任务执行。
- 重复以上步骤,形成一个持续不断的循环过程。
如何理解JS的异步?
在JavsScript中,js是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。渲染主线程承担着诸多的工作,渲染页面、执行JS都是在渲染主线程中。如果使用同步的方式,就有可能导致主线程阻塞,从而导致西澳西队列的很多其他任务无法执行。这样一来,一方面会导致繁忙的主线程白白浪费等待时间,另一方面也会给用户造成页面卡顿的现象。
所以浏览器采用异步的方法来执行,使主线程永不阻塞。具体的做法是,当某些任务执行时,比如计时器、事件监听、网络请求等操作时,主线程会将任务交给其他线程执行,等自身的任务执行完成之后,才会去执行其他任务。当其他线程执行任务时,会将事先传递的回调函数包装成任务体,加入到消息队列末尾排队,等地啊主线程执行。
实战示例分析:
🔥筑基篇:
javascript
console.log('task1')
setTimeout(() => { console.log('task2')}, 0)
Promise.resolve().then(() => { console.log('task3')})
// 执行结果:task1、task3、task2
在此例中,尽管setTimeout
设置的延迟时间为0,但是其回调函数并不会立即执行,而是排入延时宏任务队列中,而Promise.resolve().then()会立即将回调函数加入到微队列中。因此,输出顺序为 task1、task3、task2
🔥金丹篇:
javascript
function a() {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}
setTimeout(() => {
console.log(3)
Promise.resolve().then(a)
}, 0)
Promise.resolve().then(() => { console.log(4)})
console.log(5)
// 执行结果:5、4、3、1、2
- 首先,JavaScript引擎开始执行全局同步代码。
console.log(5)
被执行,输出5
。- 遇到
Promise.resolve().then(() => console.log(4))
,.then
中的回调函数被添加到微任务队列。 - 遇到
setTimeout(() => {...}, 0)
,其回调函数被放入宏任务队列,等待下一轮事件循环处理。 - 同步代码执行完毕,此时执行栈为空,开始执行微任务队列。
- 执行第一个微任务
console.log(4)
,输出4
。 - 进行下一轮事件循环,从宏任务队列中取出并执行
setTimeout
的回调函数,打印3
。 - 在
setTimeout
回调函数内调用Promise.resolve().then(a)
,将a
函数作为一个新的微任务放入微任务队列。 - 继续执行微任务队列,执行函数
a
,打印1
。 - 在函数
a
内部,新的 Promise 的.then
回调被添加到微任务队列,并随后执行,打印2
。
总结一下,整个代码的执行顺序确实是:5
-> 4
-> 3
-> 1
-> 2
🔥元婴篇:
javascript
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2")
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout1")
})
setTimeout(function () {
console.log("setTimeout2")
Promise.resolve().then(() => {
console.log("then1")
})
Promise.resolve().then(() => {
console.log("then2")
})
})
async1()
new Promise((res) => {
console.log("this is Promise");
res()
}).then(() => {
console.log("then3");
setTimeout(() => {
console.log("then3 setTimeout3")
})
})
console.log("script end")
- 同步执行全局脚本,打印
"script start"
。 - 调用
async1
函数,打印"async1 start"
。 - 在
async1
中调用async2
函数,打印"async2"
。 - 继续同步执行,创建并立即执行
new Promise
,打印"this is Promise"
,同时其.then
中的回调函数被添加到微任务队列。 - 同步执行完毕,打印
"script end"
。 - 当前执行栈为空,开始执行微任务队列。
-
- 执行
new Promise
中的.then
回调,打印"then3"
,同时在其中创建了一个setTimeout
,其回调函数被添加到宏任务队列。
- 执行
async1
函数由于遇到了await async2()
,在async2
执行完毕后恢复执行,打印"async1 end"
。- 进入下一轮事件循环,从宏任务队列中取出第一个
setTimeout
的回调函数执行,打印"setTimeout1"
。 - 继续取出第二个
setTimeout
的回调函数执行,打印"setTimeout2"
。 - 在第二个
setTimeout
的回调函数中,创建了两个Promise.resolve().then
,这两个微任务被添加到微任务队列。 - 执行微任务队列中的任务,打印
"then1"
和"then2"
。 - 再次回到宏任务队列,执行之前由
"then3"
添加的setTimeout
回调,打印"then3 setTimeout3"
。
🔥化神篇:
javascript
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => console.log('Promise inside setTimeout 1'));
process.nextTick(() => console.log('nextTick inside setTimeout 1'));
});
Promise.resolve().then(() => {
console.log('Promise 1');
new Promise((resolve) => {
console.log('Promise inside Promise 1');
resolve();
}).then(() => console.log('Nested Promise 1'));
});
process.nextTick(() => {
console.log('nextTick 1');
setTimeout(() => console.log('setTimeout inside nextTick 1'), 0);
});
new Promise((resolve) => {
console.log('Promise before setImmediate');
resolve();
}).then(() => {
setImmediate(() => {
console.log('setImmediate 1');
process.nextTick(() => console.log('nextTick inside setImmediate 1'));
});
});
console.log('script start');
// 输出结果:
Promise before setImmediate, script start, nextTick 1, Promise 1, Promise inside Promise 1, Nested Promise 1, setTimeout 1, nextTick inside setTimeout 1, Promise inside setTimeout 1,
setTimeout inside nextTick 1, setImmediate 1,nextTick inside setImmediate 1
- 同步执行,打印 "Promise before setImmediate" 和 "script start"。
- 将
setTimeout
的回调函数放入宏任务队列。 - 将第一个
Promise.resolve().then
的回调函数放入微任务队列。 - 将
process.nextTick
的回调函数放入微任务队列。 - 清空微任务队列:
-
- 打印 "nextTick 1"
- 执行 "Promise 1" 的回调,打印 "Promise 1"。
-
- 在 "Promise 1" 的回调中创建并立即执行新的 Promise,打印 "Promise inside Promise 1",并把 "Nested Promise 1" 的回调放入微任务队列。
- 清空微任务队列,打印 "Nested Promise 1"。
- 进入下一轮事件循环,执行宏任务队列中的 "setTimeout 1",打印 "setTimeout 1"。
-
- 将 "Promise inside setTimeout 1" 的回调放入微任务队列。
- 清空微任务队列,打印 "nextTick inside setTimeout 1" 和 "Promise inside setTimeout 1"。
- 执行 "setTimeout inside nextTick 1" 的回调,打印 "setTimeout inside nextTick 1"。
- 进入下一轮事件循环,执行
setImmediate
的回调,打印 "setImmediate 1"。 - 在
setImmediate
的回调中定义并执行process.nextTick
,将 "nextTick inside setImmediate 1" 的回调放入微任务队列。 - 清空微任务队列,打印 "nextTick inside setImmediate 1"。
注意:在 Node.js 环境下, setImmediate
的回调在当前宏任务队列(包括 setTimeout
)执行完毕后,但在下一轮事件循环开始之前执行。然而,process.nextTick
的回调会在当前宏任务执行完成后立即执行,即使它在 setImmediate
之后定义。
最终执行顺序:
javascript
Promise before setImmediate
script start
nextTick 1
Promise 1
Promise inside Promise 1
Nested Promise 1
setTimeout 1
nextTick inside setTimeout 1
Promise inside setTimeout 1
setTimeout inside nextTick 1
setImmediate 1
nextTick inside setImmediate 1
结语
深入理解JavaScript事件循环机制有助于我们编写更加高效、无阻塞的异步代码。希望通过对本文的学习,你能更好地把握住JavaScript执行上下文切换的关键时刻,灵活运用事件循环原理来优化程序表现,进而构建更流畅的用户体验!
本文为个人理解,可能因为经验不足导致会有差错,欢迎各位朋友批评指正。