1. 理解EventLoop:浏览器
例子:
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
分析一下:
1)刚开始整个脚本作为一个宏任务来执行,因此先打印start和end
2)setTimeout 作为一个宏任务放入宏任务队列
3)Promise.then作为一个为微任务放入到微任务队列
4)当本次宏任务执行完,检查微任务队列,发现一个Promise.then, 执行
5)接下来进入到下一个宏任务——setTimeout, 执行
因此最后的顺序是:
start
end
resolve
timeout
这样就带大家直观地感受到了浏览器环境下 EventLoop 的执行流程。不过,这只是其中的一部分情况,接下来我们来做一个更完整的总结。
1)一开始整段脚本作为第一个宏任务执行
2)执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
3)当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
4)执行浏览器 UI 线程的渲染工作
5)检查是否有Web worker任务,有则执行 (web worker 是运行在后台的JS,不会影响页面的性能。当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。)
6)执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
最后留一道题目练习:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
});
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0);
console.log('start');
2. 理解EventLoop:nodejs
nodejs 和 浏览器的 eventLoop 还是有很大差别的,值得单独拿出来说一说。
是否看过关于 nodejs 中 eventLoop 的一些文章, 是否被这些流程图搞得眼花缭乱、一头雾水:
这里会抛开这些晦涩的流程图,以最清晰浅显的方式来一步步拆解 nodejs 的事件循环机制。
三大关键阶段
首先,梳理一下 nodejs 三个非常重要的执行阶段:
1)执行定时器回调的阶段。
检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeout、setInterval。这个阶段暂且叫它 timer 。
2)轮询(英文叫 poll )阶段。
因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,
那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?
就是通过'data'、'connect'等事件使得事件循环到达 poll 阶段。
到达了这个阶段后:
如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop将回到timer阶段。
如果没有定时器, 会去看回调函数队列。
如果队列不为空,拿出队列中的方法依次执行
如果队列为空,检查是否有setImmdiate的回调
有则前往check阶段
没有则继续等待,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段。
3)check 阶段。这是一个比较简单的阶段,直接 执行 setImmdiate 的回调。
这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。
完善
首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的回调阶段 。
比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。并且在 check 阶段结束后还会进入到 关闭事件的回调阶段 。
如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。
梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:
1)timer 阶段
2)I/O 异常回调阶段
空闲、预备状态(第2阶段结束,poll 未触发之前)
3)poll 阶段
4)check 阶段
5)关闭事件的回调阶段
实例演示
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和在 11 以下的会有不同的表现。
首先说 node 版本 >= 11的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。
timer1
promise1
time2
promise2
而 node 版本小于 11 的情况下,对于定时器的处理是:
若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,
那么就将微任务暂时保存,直接去执行新的定时器任务,
当新的定时器任务执行完后,再一一执行中途产生的微任务。
因此会打印出这样的结果:
timer1
timer2
promise1
promise2
3. nodejs 和 浏览器关于eventLoop的主要区别
两者最主要的区别在于
浏览器中的微任务是在每个相应的宏任务中执行的,
而nodejs中的微任务是在不同阶段之间执行的。