这两天跟同事同事讨论遇到的一个问题,js中的event loop
,引出了chrome与node中运行具备setTimeout
和Promise
的程序时候执行结果不同的问题,从而引出了Nodejs的event loop
机制,记录一下,感受仍是蛮有收获的javascript
console.log(1) setTimeout(function() { new Promise(function(resolve, reject) { console.log(2) resolve() }) .then(() => { console.log(3) }) }, 0) setTimeout(function() { console.log(4) }, 0) // chrome中运行:1 2 3 4 // Node中运行: 1 2 4 3
chrome和Node执行的结果不同,这就颇有意思了。php
JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。
JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?
因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。html
单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着。
因而,全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。
具体来讲,异步执行的运行机制以下。(同步执行也是如此,由于它能够被视为没有异步任务的异步执行。)前端
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。java
setTimeout
与setInterval
定时器功能主要由setTimeout()
和setInterval()
这两个函数来完成,它们的内部运行机制彻底同样,区别在于前者指定的代码是一次性执行,后者则为反复执行。setTimeout(fn,0)
的含义是,指定某个任务在主线程最先可得的空闲时间执行,也就是说,尽量早得执行。它在"任务队列"的尾部添加一个事件,所以要等到同步任务和"任务队列"现有的事件都处理完,才会获得执行。HTML5
标准规定了setTimeout()
的第二个参数的最小值(最短间隔),不得低于4
毫秒,若是低于这个值,就会自动增长。在此以前,老版本的浏览器都将最短间隔设为10
毫秒。对于那些DOM的变更(尤为是涉及页面从新渲染的部分),一般不会当即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()
的效果要好于setTimeout()
。
另外,浏览器内的计时器可能会由于不少缘由而减慢速度:node
全部这些均可能将最小延迟提升到300ms甚至1000ms,具体取决于浏览器和设置。参考 Scheduling: setTimeout and setInterval
须要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等好久,因此并无办法保证,回调函数必定会在setTimeout()指定的时间执行。chrome
NodeJS的显著特色:异步机制、事件驱动。
事件轮询的整个过程没有阻塞新用户的链接,也不须要维护链接。基于这样的机制,理论上陆续有用户请求链接,NodeJS均可以进行响应,所以NodeJS能支持比Java、php程序更高的并发量。数据库
虽然维护事件队列也须要成本,再因为NodeJS是单线程,事件队列越长,获得响应的时间就越长,并发量上去仍是会力不从心。segmentfault
RESTful API是NodeJS最理想的应用场景,能够处理数万条链接,自己没有太多的逻辑,只须要请求API,组织数据进行返回便可。浏览器
关于Nodejs中的事件循环还有另外一篇文章详细探讨了下,能够参考阅读。
事件轮询主要是针对事件队列进行轮询,事件生产者将事件排队放入队列中,队列另一端有一个线程称为事件消费者会不断查询队列中是否有事件,若是有事件,就当即会执行,为了防止执行过程当中有堵塞操做影响当前线程读取队列,事件消费者线程会委托一个线程池专门执行这些堵塞操做。
Javascript前端和Node.js的机制相似这个事件轮询模型,有的人认为Node.js是单线程,也就是事件消费者是单线程不断轮询,若是有堵塞操做怎么办,不是堵塞了当前单线程的执行吗?
其实Node.js底层也有一个线程池,线程池专门用来执行各类堵塞操做,这样不会影响单线程这个主线程进行队列中事件轮询和一些任务执行,线程池操做完之后,又会做为事件生产者将操做结果放入同一个队列中。
总之,一个事件轮询Event Loop须要三个组件:
Node.js也是单线程的Event Loop,可是它的运行机制不一样于浏览器环境。
根据上图,Node.js的运行机制以下。
咱们能够看到node.js的核心其实是libuv
这个库。这个库是c写的,它可使用多线程技术,而咱们的Javascript应用是单线程的。
Nodejs 的异步任务执行流程:
用户写的代码是单线程的,但nodejs内部并非单线程!
事件机制:
Node.js不是用多个线程为每一个请求执行工做的,相反而是它把全部工做添加到一个事件队列中,而后有一个单独线程,来循环提取队列中的事件。事件循环线程抓取事件队列中最上面的条目,执行它,而后抓取下一个条目。当执行长期运行或有阻塞I/O的代码时,注意这里:它不会被阻塞,会继续提取下一个事件,而对于被阻塞的事件Node.js会从线程池中取出一个线程来运行这个被阻塞的代码,同时把当前事件自己和它的回调事件一同添加到事件队列(callback嵌套callback)。
在Node.js中,由于只有一个单线程不断地轮询队列中是否有事件,对于数据库文件系统等I/O操做,包括HTTP请求等等这些容易堵塞等待的操做,若是也是在这个单线程中实现,确定会堵塞影响其余工做任务的执行,Javascript/Node.js会委托给底层的线程池执行,并会告诉线程池一个回调函数,这样单线程继续执行其余事情,当这些堵塞操做完成后,其结果与提供的回调函数一块儿再放入队列中,当单线程从队列中不断读取事件,读取到这些堵塞的操做结果后,会将这些操做结果做为回调函数的输入参数,而后激活运行回调函数。
请注意,Node.js的这个单线程不仅是负责读取队列事件,还会执行运行回调函数,这是它区别于多线程模式的一个主要特色,多线程模式下,单线程只负责读取队列事件,再也不作其余事情,会委托其余线程作其余事情,特别是多核的状况下,一个CPU核负责读取队列事件,一个CPU核负责执行激活的任务,这种方式最适合很耗费CPU计算的任务。反过来,Node..js的执行激活任务也就是回调函数中的任务仍是在负责轮询的单线程中执行,这就注定了它不能执行CPU繁重的任务,好比JSON转换为其余数据格式等等,这些任务会影响事件轮询的效率。
看一个具体实例:
console.log('1') setTimeout(function() { console.log('2') new Promise(function(resolve) { console.log('4') resolve() }).then(function() { console.log('5') }) setTimeout(() => { console.log('6') }) new Promise(function(resolve) { console.log('7') resolve() }).then(function() { console.log('8') }) }) setTimeout(function() { console.log('9') }, 0) new Promise(function(resolve) { console.log('10') resolve() }).then(function() { console.log('11') }) setTimeout(function() { console.log('12') new Promise(function(resolve) { console.log('13') resolve() }).then(function() { console.log('14') }) }) new Promise(function(resolve) { console.log('15') resolve() }).then(function() { console.log('16') }) // node1 : 1,10,15,11,16,2,4,7,9,12,13,5,8,14,6 // 结果不稳定 // node2 : 1,10,15,11,16,2,4,7,9,5,8,12,13,14,6 // 结果不稳定 // node3 : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6 // 结果不稳定 // chrome : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6
chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种状况。
chrome运行的结果的缘由是Promise
、process.nextTick()
的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,若是取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了以后再执行宏任务队列里的任务。
关于Node中的事件循环和异步API的内容,具体能够参见另外一篇帖子,有具体讨论。
浏览器中和Node中的事件循环的执行顺序并不一致,在浏览器中,咱们能够按性质把任务分为两类,macrotask(宏任务)和 microtask(微任务)。
执行顺序:
macrotask queue
中取出第一个任务,执行完毕后,将microtask queue
中的全部任务取出,按顺序所有执行;macrotask queue
中取下一个,执行完毕后,再次将microtask queue
中的所有取出;网上的帖子大多深浅不一,甚至有些先后矛盾,在下的文章都是学习过程当中的总结,若是发现错误,欢迎留言指出~
参考:
PS:欢迎你们关注个人公众号【前端下午茶】,一块儿加油吧~
另外能够加入「前端下午茶交流群」微信群,长按识别下面二维码便可加我好友,备注加群,我拉你入群~