进击的JavaScript之再临「EventLoop」城下
先来复习几个知识点
异步任务的类型(Web APIs)
宏任务(macro-task):这个队列由事件触发线程维护
setTimeout队列=>
setInterval队列=>
setImmediate队列=>
MessageChannel=>
resquestAnimationFrame=>
I/O队列=>
UI交互事件=>
...
微任务(micro-task):这个队列由JS引擎线程维护 (因为它是在主线程下无缝执行的)
process.nextTick队列=>
Promise.then队列=>
Await=>
MutationObserver=>
...
函数调用栈(call stack)
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。无论是宏任务还是微任务,最终都是被拿到函数调用栈中执行(后进先出原则)。
事件循环(Event Loop)
“javascript引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”
(图网上找的,懒得画了🤷♂️)
从一些不得不面对的🌰来理解事件循环机制
// 🌰.1
console.log("每");
setTimeout(function () {
console.log("身");
Promise.resolve().then(() => {
console.log("不");
});
new Promise(function (resolve) {
console.log("体");
resolve();
}).then(function () {
console.log("生");
}).then(function () {
console.log("锈");
});
}, 10);//进入WebApis进程(事件触发线程),10s后放入任务队列,在第二轮宏任务中执行
setTimeout(function () {
console.log("次");
}, 0);//进入WebApis进程(事件触发线程),0s后放入任务队列,在第二轮宏任务中执行
Promise.resolve().then(() => {
console.log("动");
});
new Promise(function (resolve) {
console.log("周");
resolve("_gagaga_");
}).then(function (res) {
console.log("两");
return res;
});
//结果:每 周 动 两 Promise {<fulfilled>: '_gagaga_'} 次 身 体 不 生 锈
逐行解释:看完保证豁然开朗
首先,函数执行栈执行整体代码块script 遇到console.log("每")
,立即执行,输出 每
;遇到第一个setTimeout
,这是一个异步任务的宏任务,由WebApis进程(事件触发线程),10ms后它的第一个参数会被放进任务队列,将在第二轮宏任务中被执行;遇到第二个setTimeout
,与上一个类似,区别是0ms后它就会被放进任务队列;此时setTimeout
微任务队(左进右出,先进先出)列情况:10ms的settimeout,0ms的settimeout
;遇到第一个Promise
,Promise.resolve()
内无代码可执行,遇到.then
,这是一个微任务,将.then
里的代码加入到Promise
微任务队列;遇到第二个Promise
,new Promise
的第一个参数会立即执行,输出 周
;遇到.then
,这是本轮循环遇到的第二个微任务,将.then
里的代码加入到Promise
微任务队列(左进右出,先进先出);此时Promise
微任务队列里的情况:console.log("两") , console.log("动")
;
至此,本(第一)轮无宏任务需要执行,开始执行本轮产生的微任务。首先从微任务队列头部取出console.log("动")
执行,输出 动
,并从微任务队列中清除;再取出console.log("两")
执行,输出 两
,并从微任务队列中清除。此时微任务队列已无待执行任务,本轮循环结束,开始下一轮事件循环。
新一轮循环依然从宏任务队列开始执行:发现setTimeout
宏任务队列有任务可以执行,从队头取出0ms的settimeout
的任务执行,输出 次
;再取出10ms的settimeout
的任务执行,输出 身
;且执行过程中遇到第一个Promise
,Promise.resolve()
内无代码可执行,遇到.then
,这是一个微任务,将.then
里的代码加入到Promise
微任务队列;遇到第二个Promise
,new Promise
的第一个参数会立即执行,输出 体
;遇到.then
,这是本轮循环遇到的第二个微任务,将.then
里的代码加入到Promise
微任务队列,又遇到.then
,这是本轮循环遇到的第三个微任务,将.then
里的代码也加入到Promise
微任务队列(左进右出,先进先出);此时Promise
微任务队列里的情况:console.log("锈") console.log("生") , console.log("不")
;
至此,本(第二)轮无宏任务需要执行,开始执行本轮产生的微任务。首先从微任务队列头部取出console.log("不")
执行,输出 不
,并从微任务队列中清除;再取出console.log("生")
执行,输出 生
,并从微任务队列中清除,最后再取出console.log("锈")
执行,输出 锈
,并从微任务队列中清除。此时微任务队列已无待执行任务,本轮循环结束,开始下一轮事件循环。
新一轮循环依然从宏任务队列开始执行:发现宏任务队列中已无任务可以执行,循环结束;
这个例子的注意点:第一轮循环,遇到两个setTimeout,到了第二轮谁先执行呢?当然是延迟小的先执行。主线程之外,事件触发线程管理着一个任务队列(或消息队列),倒计时结束后,setTimeout的第一个参数会被放入宏任务队列,所以倒计时先结束的就会先放进去,当第二轮循环执行宏任务的时候也就会先执行。
// 🌰.2
console.log('start');
setTimeout(function() { //setTimeout1
console.log('setTimeout1');
process.nextTick(function() {
console.log('nextTick1'); //nextTick1
})
new Promise(function(resolve) {// Promise1
console.log('Promise1');
resolve();
}).then(function() {
console.log('Promise1.then')
})
})
new Promise(function(resolve) { //Promise2
console.log('Promise2');
resolve();
}).then(function() {
console.log('Promise2.then')
})
setTimeout(function() { //setTimeout2
console.log('setTimeout2');
process.nextTick(function() { //nextTick2
console.log('nextTick2');
})
new Promise(function(resolve) { //Promise3
console.log('Promise3');
resolve();
}).then(function() {
console.log('Promise3.then')
})
})
// start Promise2 Promise2.then setTimeout1 Promise1 nextTick1 Promise1.then setTimeout2 Promise3 nextTick2 Promise3.then
// 🌰.3
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
//执行结果:golb1, glob1_promise, glob2_promise, glob1_nextTick, glob2_nextTick, glob1_then, glob2_then, timeout1, timeout1_promise, timeout1_nextTick, timeout1_then, timeout2, timeout2_promise, timeout2_nextTick, timeout2_then, immediate1, immediate1_promise,
immediate1_nextTick, immediate1_then, immediate2, immediate2_promise, immediate2_nextTick, immediate2_then
这个参考来的栗子比较复杂,这里需要注意的点是:process.nextTick和promise.then都是微任务,但是
process.nextTick
的优先级比promise.then
要高,因此会先执行。
// 🌰.4 有await
console.log('script start');//宏任务1async function async1() {
await async2();
console.log('async1 end');//微任务1
}
async function async2() {
console.log('async2 end');//宏任务2
}
async1()
setTimeout(function() {
console.log('setTimeout');//宏任务2
}, 0)
new Promise(resolve => {
console.log('Promise');//宏任务3
resolve()
}).then(function() {
console.log('promise1');//微任务2
}).then(function() {
console.log('promise2');//微任务3
})
console.log('script end');//宏任务4//先执行完await后面的函数async2后(async2后面的console.log('async1 end')可以理解为进入到了微任务队列里),然后跳出当前async1函数,执行async1外面的同步代码,同步代码执行完,再回到async内部,最后在执行console.log( 'async1 end' );
这个栗子的注意点:对于
async/await
函数,可以将紧跟着await
后的代码当作立即执行函数(同步任务),await
的下一行当作promise.then
,即产生一个微任务。
分析过程(啰嗦点)
执行script代码块
遇到宏任务:将任务添加到对应的宏任务队列中
•遇到setTimeout,将setTimeout回调内容添加到setTimeout队列中;•遇到setImmediate,将setImmediate回调内容添加到SetImmediate队列中;
遇到微任务:将任务添加到对应的微任务队列中
•遇到process.nextTick:将回调内容添加到nextTick队列中;•遇到Promise:Promise里是立即执行函数,所以里面的代码会立即执行;Promise.then回调添加到Promise队列中;
当前宏任务执行完毕
开始执行当前宏任务所产生的微任务
•先执行nextTick队列,过程中遇到其他宏任务/微任务重复上面的逻辑•再执行Promise队列,过程中遇到其他宏任务/微任务重复上面的逻辑
所有微任务执行完毕,本轮循环结束,开始下一轮事件循环,再次从宏任务队列开始执行
•先执行setTimeout队列,过程中遇到其他宏任务/微任务重复上面的逻辑(执行过程中,若遇到微任务,加入到对应微任务队列,然后先将微任务队列中任务执行完毕,再返回执行其他setTimeout任务)•再执行setImmediate队列,过程中遇到其他宏任务/微任务重复上面的逻辑(执行过程中,若遇到微任务,加入到对应微任务队列,然后先将微任务队列中任务执行完毕,再返回执行其他setImmediate任务)
重复(先执行宏任务,再执行宏任务产生的微任务,UI Render)流程,直到所有宏任务执行完毕,函数执行栈清空,事件循环结束。
简而言之(参考大佬的总结,并加入了UI渲染的触发时机)
执行一个宏任务(函数执行栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中(先进先出原则)
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
再多说一嘴,加深记忆
setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间
当在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了;
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务;
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了;
在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。
参考
并发模型与事件循环 MDN[1]
深入核心,详解事件循环机制[2]
EventLoop 事件循环机制详解[3]
References
[1]
并发模型与事件循环 MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop[2]
深入核心,详解事件循环机制: https://segmentfault.com/a/1190000012646373[3]
EventLoop 事件循环机制详解: https://www.icode9.com/content-4-806163.html