进击的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
