这篇文章主要介绍了nodejs中事件循环机制的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。
前端开发离不开JavaScript,Javascript是一种web前端语言,主要用于web开发中,由浏览器解析执行。而js的作用不仅仅局限于前端领域的开发,它同样可以用于服务端开发——nodejs。作为一名有理想抱负的前端,想要拓展视野,掌握一门服务器端开发语言,那么nodejs是非常好的一种选择。
因为你掌握了js开发方式就很容易上手node,并且npm包管理工具也大大提升了开发体验。nodejs以异步非阻塞I/O工作方式而闻名,其处理机制被称为事件循环。
了解node事件循环机制就能更好的了解node的事件处理方式以及异步事件的执行时机,本文主要讲解一下nodejs的事件循环机制,为后续学习node奠定基础。
一、node VS javascript
前面提到,Javascript是一种web前端语言,主要用于web开发中,由浏览器解析执行,而 node.js 是一个基于 Chrome V8 引擎的JavaScript 运行环境,因此nodejs不是一门语言,不是库,不是框架,而是一个js运行时环境,简单讲node可以解析和执行js代码。以前只有浏览器可以解析执行Js,现在node可以使js完全脱离浏览器来运行。
node.js和浏览器js存在很多区别,比如浏览器中的js包括了ecmascript、BOM、DOM,但是nodejs中的js没有BOM,DOM,只有emcscript。并且node这个js执行环境为js提供了一些服务器级别的操作API,例如:文件读写,网络服务构建,网络通信,http服务器等,这些API大都被包装到核心模块里面了。另外node的事件循环机制和浏览器js的事件循环机制也不一样。
二、JavaScript事件循环
大家对浏览器中的js事件循环已经很清楚了,为了对比这里简单再提一下。
(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)
js执行时同步和异步任务任务分别进入不同的执行环境,同步任务的进入主线程,即主执行栈,异步任务(ajax请求、settimeout、setinterval、poromise.resolve()等)进入任务队列。不同的异步任务会推入不同的任务队列,比如ajax请求、settimeout、setinterval等这些任务会被推入进宏任务队列(Macro Task),而Promise函数则会被推到微任务队列(Micro Task)。整体的事件循环过程如下:
当同步代码执行完后,主执行栈变空,开始准备执行异步任务。
主线程会检查微任务队列是否为空,如果不为空那么会遍历队列内的所有微任务将其执行完,清空微任务队列,然后再检查宏任务队列。如果微任务队列是空的,直接进入下一步。
主线程遍历宏任务队列,并执行宏任务队列中的第一个宏任务,在执行的过程中如果遇到宏任务或者微任务,则继续将他们推入到对应的任务队列,每次执行完一次宏任务都要遍历执行一下微任务队列,将其清空
执行渲染操作,更新视图
开始下一次的事件循环,重复上述步骤直至两个任务队列清空
为了加深一下影响,举一个小小的,看看以下代码会输出什么:
var le=Promise.resolve(2); console.log(le) console.log('3') Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0);
用以上的事件循环过程分析一下:
js主进程执行代码遇到Promise.resolve(2),会立即执行,将2变成一个promise对象,然后console.log(le)将le变量打印出来,打印----->Promise {<fulfilled>: 2};
console.log('3'),打印----->3
接着往下执行遇到Promise.resolve().then,这是一个异步微任务函数,推到微任务栈
下一个函数遇到setTimeout,推到宏任务队列,至此主进程空了
检查微任务队列,发现Promise.resolve().then,所以打印----->promise1,又遇到一个定时器,推至宏任务队列的最后,微任务队列空了
检查宏任务队列,取第一个宏任务执行,打印----->setTimeout1,又遇到 Promise.resolve().then,推至微任务队列
再开始下一个宏任务之前,一定会清空微任务,因此打印setTimeout1后,便会检查微任务队列,于是--->promise2
接下来又一轮事件循环,取宏任务队列当前的第一个任务执行,于是打印打印----->setTimeout2,至此宏任务和微任务队列均被清空,事件循环结束
因此输出结果是:Promise {<fulfilled>: 2},3,promise1,setTimeout1,promise2,setTimeout2。
浏览器里执行结果如下:
三、node事件循环
node的事件循环共有六个阶段,在一次事件循环中这六个阶段按顺序会一直循环执行,直至事件处理完成。六个阶段的顺序图如下:
六个阶段分别是:
timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks 阶段:执行一些系统操作的回调(比如网络通信的错误回调);
idle, prepare 阶段:仅node内部使用,可忽略
poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
check 阶段:执行 setImmediate() 的回调
close callbacks 阶段:执行 socket 的 close 事件回调,如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发
事件循环中,每当进入某一个阶段,都会从该阶段对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,该阶段就会终止,然后检查NextTick队列和微任务队列,将其清空,之后进入下一个阶段。
这里面比较关键的是poll阶段:
poll队列不为空的时候,事件循环会遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
poll队列为空的时候,就会有两种情况:
如果代码中存在setImmediate()回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调;
如果不存在setImmediate()回调,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,如果规定时间内有定时器函数进入队列,则返回到timer阶段,执行定时器回调,否则在poll阶段等待回调进入队列。
同样的举个大大的,看看以下代码会输出什么:
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事件循环分析呗:
先执行同步任务,打印start,end
进入timer阶段前,清空NextTick和micro队列,所以打印promise3
进入timer阶段,打印timer1,并发现有一个微任务,立即执行微任务,打印promise1
仍然在timer阶段,执行下个宏任务,打印timer2,同样遇到微任务,立即执行,打印promise2
因此输出顺序是:start,end,promise3,timer1,promise1,timer2,promise2,如果能正确回答出来说明对node的循环机制有了大体的了解,实际node输出结果确实是这样:
那如下代码会输出什么呢?
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);});
继续分析:
process.nextTick会将任务推进至nextTick队列,promise.then会把任务推至micro队列,上面提到过每次一个宏任务执行完,执行下一个宏任务之前需要清空nextTick队列和micro队列,同样的一个阶段执行完,进入下一个阶段之前也需要nextTick队列和micro队列,并且nextTick队列优先级高于micro队列
先执行同步代码,打印3,4
执行nextTick队列,打印7,8
再执行micro队列,打印5
因此最终输出是:3,4,7,8,5,需要记住,process.nextTick 永远大于 promise.then的优先级
还有一个大家很容易混淆的点就是setTimout和setImmediate的执行时机,根据上面描述的node事件循环机制,setImmediate()应该在check阶段执行 与 而setTimeout在timer阶段执行,理论上setTimout比setImmediate先执行,看下面的代码:
setTimeout(() => console.log(1),0);setImmediate(() => console.log(2));
执行结果是什么?1,2 还是 2,1,其实都有可能,看实际node运行的结果:
可以看到两次执行的结果不一样,为什么呢?原因在于即使setTimeout的第二个参数默认为0,但实际上,Node做不到0秒就执行其回调,最少也要4毫秒。那么进入事件循环后,如果没到4毫秒,那么timers阶段就会被跳过,从而进入check阶段执行setImmediate回调,此时输出结果是:2,1;
如果进入事件循环后,超过4毫秒(只是个大概,具体值并不确定),setTimeout的回调会出现在timer阶段的队列里,回调将被执行,之后再进入poll阶段和check阶段,此时输出结果是:1,2
那如果两者在I/O周期内调用,谁先执行呢?看一下代码:
const fs = require('fs')fs.readFile('./test.txt', 'utf8' , (err, data) => { if (err) { console.error(err) return } setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });})
实际上,node中输出的结果总是immediate先输出,timeout后输出。因为I/O回调是在poll阶段执行,当回调执行完毕之后队列为空,发现存在setImmediate的回调就会进入check阶段,执行完毕后,再进入timer阶段。
感谢你能够认真阅读完这篇文章,希望小编分享的“nodejs中事件循环机制的示例分析”这篇文章对大家有帮助,同时也希望大家多多支持编程网,关注编程网行业资讯频道,更多相关知识等着你来学习!