nodejs 和 浏览器的 eventLoop 还是有很大差别的,值得单独拿出来说一说。
不知你是否看过关于 nodejs 中 eventLoop 的一些文章, 是否被这些流程图搞得眼花缭乱、一头雾水:
看到这你不用紧张,这里会抛开这些晦涩的流程图,以最清晰浅显的方式来一步步拆解 nodejs 的事件循环机制。
首先,梳理一下 nodejs 三个非常重要的执行阶段:
定时器回调
的阶段。检查定时器,如果到了时间,就执行回调。这些定时器就是setTimeout、setInterval。这个阶段暂且叫它timer
。poll
)阶段。因为在node代码中难免会有异步操作,比如文件I/O,网络I/O等等,那么当这些异步操作做完了,就会来通知JS主线程,怎么通知呢?就是通过'data'、 'connect'等事件使得事件循环到达 poll
阶段。到达了这个阶段后:如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到timer阶段。
如果没有定时器, 会去看回调函数队列。
不为空
,拿出队列中的方法依次执行为空
,检查是否有 setImmdiate
的回调check阶段
(下面会说)没有则继续等待
,相当于阻塞了一段时间(阻塞时间是有上限的), 等待 callback 函数加入队列,加入后会立刻执行。一段时间后自动进入 check 阶段
。执行 setImmdiate
的回调。这三个阶段为一个循环过程。不过现在的eventLoop并不完整,我们现在就来一一地完善。
首先,当第 1 阶段结束后,可能并不会立即等待到异步事件的响应,这时候 nodejs 会进入到 I/O异常的回调阶段
。比如说 TCP 连接遇到ECONNREFUSED,就会在这个时候执行回调。
并且在 check 阶段结束后还会进入到 关闭事件的回调阶段
。如果一个 socket 或句柄(handle)被突然关闭,例如 socket.destroy(), 'close' 事件的回调就会在这个阶段执行。
梳理一下,nodejs 的 eventLoop 分为下面的几个阶段:
是不是清晰了许多?
好,我们以上次的练习题来实践一把:
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的,它会和浏览器表现一致,一个定时器运行完立即运行相应的微任务。
timer1promise1time2promise2
而 node 版本小于 11 的情况下,对于定时器的处理是:
若第一个定时器任务出队并执行完,发现队首的任务仍然是一个定时器,那么就将微任务暂时保存,
直接去执行
新的定时器任务,当新的定时器任务执行完后,再一一执行
中途产生的微任务。
因此会打印出这样的结果:
timer1timer2promise1promise2
两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务
中执行的,而nodejs中的微任务是在不同阶段之间
执行的。
process.nextTick 是一个独立于 eventLoop 的任务队列。
在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务
执行。