We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Event Loop 是计算机中的一种运行机制,Javascript采用这种结构来解决单线程带来的一些问题,目前主要分为两种,一种是浏览器端实现的事件循环,另一种就是Node端实现的事件循环,两者有点不同,所以在执行某些代码的时候我们会发现可能同样的代码在两种不同环境下输出的结果是不同,那么为了理解这种不同的差异,我们有必要深入了解下浏览器和Node中不同环境下实现的Event loop
Event Loop
Event loop
因为JavaScript是一种单线程语言,因此为了实现主线程不阻塞,Event Loop这样的方案应运而生,
JavaScript
在浏览器中主要将任务分为两种,一种是macro task(微任务),一种是micro task(宏任务)
macro task主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件
micro task主要包含:Promise、process.nextTick、MutaionObserver
根据规范来讲,浏览器主要是这样处理的:
Task
console.log(1) setTimeout(() => { console.log(2) new Promise(resolve => { console.log(4) resolve() }).then(() => { console.log(5) }) }) new Promise(resolve => { console.log(7) resolve() }).then(() => { console.log(8) }) setTimeout(() => { console.log(9) new Promise(resolve => { console.log(11) resolve() }).then(() => { console.log(12) }) })
这个函数的执行结果为1、7、8、2、4、5、9、11、12
1、7、8、2、4、5、9、11、12
Node中的Event loop底层是libuv的event loop,Node也是将任务分为宏任务和微任务两种类型,整个事件循环过程如下:
┌───────────────────────┐ ┌─>│ timers │<————— 执行 setTimeout()、setInterval() 的回调 │ └──────────┬────────────┘ | |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调 │ ┌──────────┴────────────┐ │ │ pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略) │ └──────────┬────────────┘ | |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调 │ ┌──────────┴────────────┐ │ │ idle, prepare │<————— 内部调用(可忽略) │ └──────────┬────────────┘ | |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调 | | ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段) │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ | | | | | └───────────────┘ | |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调 | ┌──────────┴────────────┐ │ │ check │<————— setImmediate() 的回调将会在这个阶段执行 │ └──────────┬────────────┘ | |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调 │ ┌──────────┴────────────┐ └──┤ close callbacks │<————— socket.on('close', ...) └───────────────────────┘
setTimeout
setInterval
setImmediate
socket.on('colse',...)
Node 中宏任务分为了几种,并且每一种都存在自己的task queue, 同时我们可以将Process.nextTick看做是一种微任务,优于其他所有的微任务而执行,并且所有 MicroTask(微任务) 的执行时机,是不同类型 MacroTask(宏任务) 切换的时候,执行过程可以参考下面这张图:
先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(注意 NextTick 要优先哦); 进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask; 再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask; 再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask; 至此,完成一个 Tick,回到 timers 阶段;
setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') })
虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate
const fs = require('fs') fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) })
这个函数可以保证函数执行在Poll阶段,然后是check阶段,所以首次应该是setImmediate执行,本质是我们执行里面回调函数是在poll阶段,这个阶段的下一个阶段直接就是check,所以后面首先会进入check阶段执行setImmediate的任务
poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况:
在很多文章中,将 pending callbacks 阶段都写作 I/O callbacks 阶段,并说在此阶段,执行了除 close callbacks、 timers、setImmediate以外的几乎所有的回调,也就是把 poll 阶段的工作与此阶段的工作混淆了。 在我阅读时,就曾产生过疑问,假如大部分回调是在 I/O callbacks 阶段执行的,那么 poll 阶段就没有理由阻塞,因为你并不能保证“无事可做”,你得去 I/O callbacks 阶段检查一下才知道嘛! 所以最终结合其他几篇文章以及对源码的分析,应该可以确定,I/O callbacks 更准确的叫做 pending callbacks,它所执行的回调是比较特殊的、且不需要关心的,而真正重要的、大部分回调所执行的阶段是在 poll 阶段。
查阅了libuv 的文档后发现,在 libuv 的 event loop 中,I/O callbacks 阶段会执行 Pending callbacks。绝大多数情况下,在 poll 阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在 I/O callbacks 阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。
I/O callbacks
Pending callbacks
poll
根据上文中整个事件流程,我们可以知道,process.nextTick()会在各个事件阶段之间执行,一旦执行,要等到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用process.nextTick,这样的话就不能保证Node进入下一个Tick,这样的话就有可能带着IO的callback函数不能是实现,出现IO饥饿问题。
这个要从从自己的实际开发和项目要求来说,入股为了避免阻塞IO,应该使用setImmediate,因为他的执行在poll阶段之后执行。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Event Loop
是计算机中的一种运行机制,Javascript采用这种结构来解决单线程带来的一些问题,目前主要分为两种,一种是浏览器端实现的事件循环,另一种就是Node端实现的事件循环,两者有点不同,所以在执行某些代码的时候我们会发现可能同样的代码在两种不同环境下输出的结果是不同,那么为了理解这种不同的差异,我们有必要深入了解下浏览器和Node中不同环境下实现的Event loop
浏览器中实现的Event Loop
因为
JavaScript
是一种单线程语言,因此为了实现主线程不阻塞,Event Loop这样的方案应运而生,在浏览器中主要将任务分为两种,一种是macro task(微任务),一种是micro task(宏任务)
macro task主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件
micro task主要包含:Promise、process.nextTick、MutaionObserver
根据规范来讲,浏览器主要是这样处理的:
Task
这个函数的执行结果为
1、7、8、2、4、5、9、11、12
Node中实现的EventLoop
Node中的Event loop底层是libuv的event loop,Node也是将任务分为宏任务和微任务两种类型,整个事件循环过程如下:
setTimeout
和setInterval
的回调函数setImmediate
排定的之外),其余情况node将在此处阻塞socket.on('colse',...)
Node的事件循环和浏览器本质不同
Node 中宏任务分为了几种,并且每一种都存在自己的task queue, 同时我们可以将Process.nextTick看做是一种微任务,优于其他所有的微任务而执行,并且所有 MicroTask(微任务) 的执行时机,是不同类型 MacroTask(宏任务) 切换的时候,执行过程可以参考下面这张图:
细节问题
细节一:setTimeout 与 setImmediate 的顺序
虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate
这个函数可以保证函数执行在Poll阶段,然后是check阶段,所以首次应该是setImmediate执行,本质是我们执行里面回调函数是在poll阶段,这个阶段的下一个阶段直接就是check,所以后面首先会进入check阶段执行
setImmediate
的任务细节二:poll 阶段
poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况:
细节三:关于 pending callbacks 阶段
在很多文章中,将 pending callbacks 阶段都写作 I/O callbacks 阶段,并说在此阶段,执行了除 close callbacks、 timers、setImmediate以外的几乎所有的回调,也就是把 poll 阶段的工作与此阶段的工作混淆了。
在我阅读时,就曾产生过疑问,假如大部分回调是在 I/O callbacks 阶段执行的,那么 poll 阶段就没有理由阻塞,因为你并不能保证“无事可做”,你得去 I/O callbacks 阶段检查一下才知道嘛!
所以最终结合其他几篇文章以及对源码的分析,应该可以确定,I/O callbacks 更准确的叫做 pending callbacks,它所执行的回调是比较特殊的、且不需要关心的,而真正重要的、大部分回调所执行的阶段是在 poll 阶段。
细节四: IO饥饿问题
根据上文中整个事件流程,我们可以知道,process.nextTick()会在各个事件阶段之间执行,一旦执行,要等到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用process.nextTick,这样的话就不能保证Node进入下一个Tick,这样的话就有可能带着IO的callback函数不能是实现,出现IO饥饿问题。
细节五:如何合理使用setImmediate和setTimeout
这个要从从自己的实际开发和项目要求来说,入股为了避免阻塞IO,应该使用setImmediate,因为他的执行在poll阶段之后执行。
参考资料
The text was updated successfully, but these errors were encountered: