在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Node事件循环Node底层使用的语言libuv,是一个c++语言。他用来操作底层的操作系统,封装了操作系统的接口。Node的事件循环也是用 因为Node和操作系统打交道,所以事件循环比较复杂,也有一些自己特有的API。 事件循环图说好的一张图,也不卖关子。下边这张图搞清楚了,事件循环就学会了。 事件循环图 事件循环图-结构 为了让大家先有个大局观,先贴一张目录结构图在前边: 目录 接下来详细展开说说 主线程主线程 上图中,几个色块的含义:
事件循环 圈事件循环 圈 图中灰色的圈跟操作系统有关系,不是本章解析重点。重点关注黄色、橙色的圈还有中间橘黄的方框。 我们把每一圈的事件循环叫做「一次循环」、又叫「一次轮询」、又叫「一次Tick」。 一次循环要经过六个阶段:
本次我们只关注上边标红的三个重点。 工作原理
其中, timers队列的工作原理timers并非真正意义上的队列,他内部存放的是计时器。 检查过程:将每一个计时器按顺序分别计算一遍,计算该计时器开始计时的时间到当前时间是否满足计时器的间隔参数设定(比如1000ms,计算计时器开始计时到现在是否有1m)。当某个计时器检查通过,则执行其回调函数。 poll队列的运作方式
举例梳理事件流程setTimeout(() => { console.log('object'); }, 5000) console.log('node'); 以上代码的事件流程梳理 进入主线程,执行setTimeout(),回调函数作为异步任务被放入异步队列timers队列中,暂时不执行。
要理解这个问题,看下边的代码及流程解析: setTimeout(function t1() { console.log('setTimeout'); }, 5000) console.log('node 生命周期'); const http = require('http') const server = http.createServer(function h1() { console.log('请求回调'); }); server.listen(8080) 代码分析如下:
六个队列都没任务,则在poll队列等待。如下图:
无限循环…… 梳理事件循环流程图: 注意:下图中的“是否有任务”的说法表示“是否有本队列的任务”。 event loop流程梳理 再用一个典型的例子验证下流程: const startTime = new Date(); setTimeout(function f1() { console.log('setTimeout', new Date(), new Date() - startTime); }, 200) console.log('node 生命周期', startTime); const fs = require('fs') fs.readFile('./poll.js', 'utf-8', function fsFunc(err, data) { const fsTime = new Date() console.log('fs', fsTime); while (new Date() - fsTime < 300) { } console.log('结束死循环', new Date()); }); 连续运行三遍,打印结果如下: 执行流程解析:
等待时间够长后,向下回到event loop。 event loop检查没有其他异步任务了,结束线程,整个程序over退出。 check 阶段检查阶段(使用 setImmediate 的回调会直接进入这个队列) check队列的实际工作原理 真正的队列,里边扔的就是待执行的回调函数的集合。类似[fn,fn]这种形式的。 所以说,setImmediate不是一个计时器的概念。 如果你去面试,涉及到Node环节,可能会遇到下边这个问题:setImmediate和setTimeout(0)谁更快。 setImmediate() 与 setTimeout(0) 的对比
综上,setImmediate的运算速度比setTimeout(0)的要快,因为setTimeout还需要开计时器线程,并增加计算的开销。 二者的效果差不多。但是执行顺序不定 观察以下代码: setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }); 多次反复运行,执行效果如下: 顺序不定 可以看到多次运行,两句console.log打印的顺序不定。 以上代码,主线程运行的时候,setTimeout函数调用,计时器线程增加一个定时器任务。setImmediate函数调用后,其回调函数立即push到check队列。主线程执行完毕。 eventloop判断时,发现timers和check队列有内容,进入异步轮询: 第一种情况:等到了timers里这段时间,可能还没有1ms的时间,定时器任务间隔时间的条件不成立所以timers里还没有回调函数。继续向下到了check队列里,这时候setImmediate的回调函数早已等候多时,直接执行。而再下次eventloop到达timers队列,定时器也早已成熟,才会执行setTimeout的回调任务。于是顺序就是「setImmediate -> setTimeout」。 第二种情况:但也有可能到了timers阶段时,超过了1ms。于是计算定时器条件成立,setTimeout的回调函数被直接执行。eventloop再向下到达check队列执行setImmediate的回调。最终顺序就是「setTimeout -> setImmediate」了。 所以,只比较这两个函数的情况下,二者的执行顺序最终结果取决于当下计算机的运行环境以及运行速度。 二者时间差距的对比代码 ------------------setTimeout测试:------------------- let i = 0; console.time('setTimeout'); function test() { if (i < 1000) { setTimeout(test, 0) i++ } else { console.timeEnd('setTimeout'); } } test(); ------------------setImmediate测试:------------------- let i = 0; console.time('setImmediate'); function test() { if (i < 1000) { setImmediate(test) i++ } else { console.timeEnd('setImmediate'); } } test(); 运行观察时间差距: setTimeout与setImmediate时间差距 可见setTimeout远比setImmediate耗时多得多 这是因为setTimeout不仅有主代码执行的时间消耗。还有在timers队列里,对于计时器线程中各个定时任务的计算时间。 结合poll队列的面试题(考察timers、poll和check的执行顺序)如果你看懂了上边的事件循环图,下边这道题难不倒你! // 说说下边代码的执行顺序,先打印哪个? const fs = require('fs') fs.readFile('./poll.js', () => { setTimeout(() => console.log('setTimeout'), 0) setImmediate(() => console.log('setImmediate')) }) 上边这种代码逻辑,不管执行多少次,肯定都是先执行setImmediate。 先执行setImmediate 因为fs各个函数的回调是放在poll队列的。当程序holding在poll队列后,出现回调立即执行。 nextTick 与 Promise说完宏任务,接下来说下微任务
nextTick表现形式 process.nextTick(() => {}) Promise表现形式 Promise.resolve().then(() => {}) 如何参与事件循环? 事件循环中,每执行一个回调前,先按序清空一次nextTick和promise。 // 先思考下列代码的执行顺序 setImmediate(() => { console.log('setImmediate'); }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) }) 最终顺序:
两个问题: 基于上边的说法,有两个问题待思考和解决:
上边两个问题,看下边代码的说法 setTimeout(() => { console.log('setTimeout 100'); setTimeout(() => { console.log('setTimeout 100 - 0'); process.nextTick(() => { console.log('nextTick in setTimeout 100 - 0'); }) }, 0) setImmediate(() => { console.log('setImmediate in setTimeout 100'); process.nextTick(() => { console.log('nextTick in setImmediate in setTimeout 100'); }) }); process.nextTick(() => { console.log('nextTick in setTimeout100'); }) Promise.resolve().then(() => { console.log('promise in setTimeout100'); }) }, 100) const fs = require('fs') fs.readFile('./1.poll.js', () => { console.log('poll 1'); process.nextTick(() => { console.log('nextTick in poll ======'); }) }) setTimeout(() => { console.log('setTimeout 0'); process.nextTick(() => { console.log('nextTick in setTimeout'); }) }, 0) setTimeout(() => { console.log('setTimeout 1'); Promise.resolve().then(() => { console.log('promise in setTimeout1'); }) process.nextTick(() => { console.log('nextTick in setTimeout1'); }) }, 1) setImmediate(() => { console.log('setImmediate'); process.nextTick(() => { console.log('nextTick in setImmediate'); }) }); process.nextTick(() => { console.log('nextTick 1'); process.nextTick(() => { console.log('nextTick 2'); }) }) console.log('global ------'); Promise.resolve().then(() => { console.log('promise 1'); process.nextTick(() => { console.log('nextTick in promise'); }) }) /** 执行顺序如下 global ------ nextTick 1 nextTick 2 promise 1 nextTick in promise setTimeout 0 // 解释问题1. 没有上边的nextTick和promise,setTimeout和setImmediate的顺序不一定,有了以后肯定是0先开始。 // 可见,执行一个队列之前,就先检查并执行了nextTick和promise微队列 nextTick in setTimeout setTimeout 1 nextTick in setTimeout1 promise in setTimeout1 setImmediate nextTick in setImmediate poll 1 nextTick in poll ====== setTimeout 100 nextTick in setTimeout100 promise in setTimeout100 setImmediate in setTimeout 100 nextTick in setImmediate in setTimeout 100 setTimeout 100 - 0 nextTick in setTimeout 100 - 0 */ 以上代码执行多次,顺序不变,setTimeout和setImmediate的顺序都没变。 执行顺序及具体原因说明如下:
扩展:为什么有了setImmediate还要有nextTick和Promise? 一开始设计的时候,setImmediate充当了微队列的作用(虽然他不是)。设计者希望执行完poll后立即执行setImmediate(当然现在也确实是这么表现的)。所以起的名字叫 于是出现nextTick,真正的微队列概念。但此时,immediate的名字被占用了,所以名字叫nextTick(下一瞬间)。事件循环期间,执行任何一个队列之前,都要检查他是否被清空。其次是Promise。 面试题最后,检验学习成果的面试题来了 async function async1() { console.log('async start'); await async2(); console.log('async end'); } async function async2(){ console.log('async2'); } console.log('script start'); setTimeout(() => { console.log('setTimeout 0'); }, 0) setTimeout(() => { console.log('setTimeout 3'); }, 3) setImmediate(() => { console.log('setImmediate'); }) process.nextTick(() => { console.log('nextTick'); }) async1(); new Promise((res) => { console.log('promise1'); res(); console.log('promise2'); }).then(() => { console.log('promise 3'); }); console.log('script end'); // 答案如下 // - // - // - // - // - // - // - // - // - // - // - // - /** script start async start async2 promise1 promise2 script end nextTick async end promise 3 // 后边这仨的运行顺序就是验证你电脑运算速度的时候了。 速度最好(执行上边的同步代码 + 微任务 + 计时器运算用了不到0ms): setImmediate setTimeout 0 setTimeout 3 速度中等(执行上边的同步代码 + 微任务 + 计时器运算用了0~3ms以上): setTimeout 0 setImmediate setTimeout 3 速度较差(执行上边的同步代码 + 微任务 + 计时器运算用了3ms以上): setTimeout 0 setTimeout 3 setImmediate */ 思维脑图
Node生命周期核心阶段 到此这篇关于全面了解Node事件循环的文章就介绍到这了,更多相关Node事件循环内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论