事件循环机制
浏览器
阐述
JS脚本scrpit包含异步任务和同步任务,首先同步任务在主线程上执行,当同步任务执行完成后,JS将当前执行栈清空。然后开始执行异步任务,异步任务分为宏任务和微任务,异步任务会放入任务队列当中,宏任务放入宏任务队列,微任务会放入微任务队列。
同步任务执行完之后,先去微任务队列拿出一个微任务,如果执行过程产生了微任务或者宏任务,会将他们放入各自的任务队列。循环往复,直到把微任务队列清空。
紧接着会从宏任务队列中拿出一个宏任务,执行过程中若产生了微任务,首先会把微任务放入微任务队列,然后等这个宏任务执行完成之后就会立即去执行这个微任务,如果没有产生微任务就继续执行下一个宏任务,循环往复,直到清空宏任务。
在宏任务执行完成前的一刻,渲染进程会触发GUI和dom的渲染。
例题分析
console.log('script start');
setTimeout(function() { console.log('setTimeout'); }, 0);
Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
|
查看答案
script start script end promise1 promise2 setTimeout
执行栈: script start script end 微任务队列: promise1 promise2 宏任务队列: setTimeout
|
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(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') })
console.log('script end')
|
查看答案
script start async2 end async1 end promise1 promise2 setTimeout
分析: async 和 await 代码转换为
async function async1() { await async2() console.log('async1 end') } ===》
function f() { return RESOLVE(p).then(() => { console.log('async1 end') }) }
首先执行同步任务
- script start - async2 end - Promise (promise的构造函数将其视为同步函数) - script end 然后将webapi将异步任务队列分别丢进微任务队列和宏任务队列
首先执行微任务,执行微任务队列前会去检查当前任务队列是否存在在微任务队列或执行栈任务, 有就放入各自队列,直到清空微任务
- async1 end - promise1 - promise2 - setTimeout 然后执行宏任务,执行宏任务前会去检查当前宏任务的微任务队列,若存在则一次性执行完, 不存在则执行宏任务。宏任务执行完成,渲染进程触发GUI,dom渲染。 依次循环
|
注意这里,如果浏览器的版本不一致,可能也导致不一样的结果
关于73以下版本和73版本的区别
- 在老版本版本以下,先执行
promise1
和 promise2
,再执行 async1
。
- 在73版本,先执行
async1
再执行 promise1
和 promise2
。
NodeJS
阐述
Node的事件循环会经历6个阶段

timers
: 执行setTimeout和setInterval到了时间的回调
pending back
: 上一轮循环少数的 callback
会放在这一阶段执行
idle,prepare
: 内部使用
poll
: 最重要的阶段,中文叫做轮询阶段
::: tip 完整解释
像诸如 文件I/O 网络I/O等等这些一步操作执行完,会通知JS主线程。如何通知呢?
它会通过 data
、connect
等事件使得事件循环到达轮询阶段,到了这个阶段之后如果:
- 如果定时器到期了,会首先回到
timers
阶段
- 如果没有定时器,看回调函数队列
- 队列不为空: 依次执行队列的回调
- 队列为空:检查是否有
setImmdiate
回调
- 有:进入
check
阶段
- 没有:继续等待,阻塞一段时间然后自动进入
check阶段
:::
check
: 直接执行 setImmdiate
阶段
close callback
: 执行close事件的 callback
,例如socket.on('close',fn)
等
**为什么要使用process.nextTick()**,因为可以允许用户处理错误,或者是在事件循环继续之前重试请求
总结规律
在 文件I/O 、 网络I/O中,setImmediate会 先于 settimeout 否则一般情况下 setTimeout 会先于 setImmediate
|
Node版本对事件循环的影响
node 版本 >= 11的(我们现在用的),它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。
node 版本 < 11 ,当一个定时器运行完之后,会将过程中产生的微任务暂存,然后直接执行新的定时器任务,最后逐一执行完微任务
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0)
node <11: timer1,timer2,promise1,promise2 node >=11: timer1,promise1,timer2,promise2
|
例题分析
以下试题都是以Node环境执行结果
console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end')
|
查看答案
node >= 11: start,end,timer1,promise1,timer2,promise2
node < 11: start,end,timer1,timer2,promise1,promise2
首先执行主线程执行栈任务:打印start,end
然后执行微任务Promise,这一步和浏览器一致,打印promise3
然后进入timers阶段,这里会执行定时器到期的回调
如果node版本>=11, 则代表执行完一个定时器立即执行callback里面的微任务,打印timer1,promise1,timer2,promise2
如果node版本<11,则代表执行完定时器暂存微任务,继续执行定时器任务,最后执行微任务,打印timer1,timer2,promise1,promise2
|
setTimeout(() => { console.log('timeout'); }, 0);
setImmediate(() => { console.log('immediate'); });
const fs = require('fs');
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
|
查看答案
timeout,immediate,immediate,timeout
首先需要掌握:immediate的机制 1. 有IO操作时,immediate其实就是轮询完成之后立即执行的函数, 如果有IO操作调度的话,他会优先于定时器函数,它的执行是发生在任务队列的尾部 2. 无IO操作,immediate会比定时器晚执行
所以依此规则:读取IO发生在 `timers` 阶段之后,打印timeout,immediate,immediate,timeout
|
超经典试题
let bar console.log('start') const fs = require('fs');
fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end')
function someAsyncApiCall(callback) { process.nextTick(callback); }
someAsyncApiCall(() => { console.log('bar', bar); });
bar = 1
|
查看答案
node >=11: start,end,bar:1,promise3,timer1,promise1,timer2,promise2,immediate,timeout
node <11: start,end,bar:1,promise3,timer1,timer2,promise1,promise2,immediate,timeout
首先执行主线程的执行栈任务,打印start,end
process.nextTick的机制是发生在执行栈尾部,优先微任务,所以打印 bar:1
执行任务队列的微任务队列,打印promise3
IO操作发生在poll轮询阶段,所以先看timers阶段的定时器, 由于node版本问题会造成不同的答案,所以会有两种答案
node >= 11 打印timer1,promise1,timer2,promise2 node <11 打印timer1,timer2,promise1,promise2
接着到达poll阶段,由于有文件IO操作, 此时的seImmediate也就是check阶段调用的函数会优先于定时器先执行,所以打印 immediate timeout
|
浏览器和Node事件循环的区别
浏览器环境下微任务的执行是每个宏任务执行之后,而node中微任务会在各个阶段之间执行,
一个阶段结束立刻执行 mircroTask
异步输出结果
- await 后面接一个会 return new promise 的函数并执行它
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('setTimeout') }, 0) async1() new Promise(function (resolve) { console.log('promise1') resolve() }).then(function () { console.log('promise2') }) console.log('script end')
|
输出: script start async1 start async2 promise1 script end async1 end promise2 setTimeout
|
- process.nextTick()要优于promise.then执行
node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。
process.nextTick(function(){ console.log(7); });
new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); });
process.nextTick(function(){ console.log(8); });
|
async function f(){ await new Promise(resolve => { setTimeout(() => { console.log('1'); }, 2000); }) await new Promise(resolve => { setTimeout(() => { console.log('2'); }, 3000); })
console.log('3'); }
f()
|
async function f2() { let promiseA = new Promise(resolve => { setTimeout(() => { console.log('1'); }, 2000); })
let promiseB = new Promise(resolve => { setTimeout(() => { console.log('2'); }, 3000); })
await promiseA; await promiseB; console.log('3'); } f2()
|