这两天跟同事和同事讨论了一个问题。js中的事件循环导致在chrome和node中运行带有setTimeout和Promise的程序时执行结果不同。问题,引出Nodejs的事件循环机制,记录一下,感觉挺有收获的console.log(1)setTimeout(function(){newPromise(function(resolve,reject){console.log(2)resolve()}).then(()=>{console.log(3)})},0)setTimeout(function(){console.log(4)},0)//在chrome中运行:1234//在Node中运行:1243chrome和Node执行的结果不一样,很有意思。一、JS中的任务队列JavaScript语言的一大特点就是单线程,也就是说同一时间只能做一件事。那么,为什么JavaScript不能有多线程呢?这样可以提高效率啊。JavaScript的单线程与它的使用有关。作为一种浏览器脚本语言,JavaScript的主要目的是与用户交互和操作DOM。这就决定了它只能是单线程的,否则会带来非常复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向某个DOM节点添加内容,另一个线程删除这个节点,那么浏览器应该以哪个线程为基础呢?因此,为了避免复杂性,JavaScript从一开始就是单线程的,这已经成为这门语言的核心特征,以后也不会改变。为了利用多核CPU的计算能力,HTML5提出了WebWorker标准,允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,不得操作DOM。因此,这个新标准并没有改变JavaScript的单线程特性。2.任务队列事件循环单线程是指所有的任务都需要排队,只有上一个任务完成后才会执行下一个任务。如果前一个任务耗时很长,后一个任务就得一直等下去。因此,所有的任务可以分为两种,一种是同步任务(synchronous),一种是异步任务(asynchronous)。同步任务是指在主线程上排队等待执行的任务,只有上一个任务执行完才能执行下一个任务;异步任务是指不进入主线程而是进入“任务队列”(taskqueue)的任务,只有当“任务队列”通知主线程有异步任务可以执行时,任务才会进入执行的主线程。具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以看作是异步执行,没有异步任务。)所有同步任务都在主线程上执行,形成一个执行上下文栈。除了主线程之外,还有一个“任务队列”。只要异步任务有运行结果,就会在“任务队列”中放入一个事件。一旦“执行栈”中的所有同步任务都执行完毕,系统就会读取“任务队列”,看看里面有什么事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。只要主线程是空的,它就会读取“任务队列”,这是JavaScript的运行机制。这个过程一遍又一遍地重复。3、定时器setTimeout和setInterval定时器函数主要由setTimeout()和setInterval()这两个函数完成。它们的内部运行机制是完全一样的。不同的是前者指定的代码只执行一次,而后者是重复执行。实施。setTimeout(fn,0)的意思是指定一个任务在主线程最早可用的空闲时间执行,也就是尽可能早的执行。它将一个事件添加到“任务队列”的末尾,因此它不会被执行,直到同步任务和“任务队列”的现有事件都被处理完。HTML5标准规定setTimeout()的第二个参数的最小值(最短间隔)不得低于4毫秒,低于这个值会自动增加。在此之前,旧版浏览器将最小间隔设置为10毫秒。对于那些DOM变化(尤其是那些涉及页面重新渲染的变化),它们通常不会立即执行,而是每16毫秒执行一次。这时候使用requestAnimationFrame()的效果比setTimeout()要好。此外,浏览器内部的计时器可能会因多种原因而变慢:CPU过载后台模式下的浏览器选项卡使用电池的笔记本电脑所有这些都可能将最小延迟增加到300毫秒甚至1000毫秒,具体取决于浏览器和设置。参考调度:setTimeout和setInterval。需要注意的是,setTimeout()只是将事件插入“任务队列”,主线程只有在当前代码(执行栈)执行完后才会执行它指定的回调函数。如果当前代码耗时很长,可能要等待很长时间,所以没办法保证回调函数会在setTimeout()指定的时间执行。4.Nodejs特点NodeJS的显着特点:异步机制,事件驱动。整个事件轮询过程不会阻塞新用户连接,也不需要维护连接。基于这种机制,理论上NodeJS可以响应用户一个接一个的连接请求,因此NodeJS可以支持比Java和PHP程序更高的并发。虽然维护事件队列也是需要成本的,而且由于NodeJS是单线程的,事件队列越长,得到响应的时间就越长,并发量还是会不足。RESTfulAPI是NodeJS最理想的应用场景。它可以处理数以万计的连接。它本身并没有太多的逻辑。它只需要请求API并组织数据返回。5.Node.js的事件循环关于Nodejs中的事件循环,还有一篇文章讲的很详细,大家可以参考阅读。事件轮询主要是轮询事件队列。事件生产者将事件排入队列,队列的另一端有一个线程称为事件消费者,它会不断查询队列中是否有事件。如果有事件发生,它会立即执行,为了防止阻塞操作在执行过程中影响当前线程的读队列,事件消费者线程会委托一个线程池来执行这些阻塞操作。Javascript前端和Node.js的机制类似于这种事件轮询模型。有人认为Node.js是单线程的,即事件消费者是单线程的,不断轮询。有阻塞操作怎么办,不是阻塞当前单线程Thread执行?其实Node.js底层也有一个线程池。线程池专门用于执行各种阻塞操作。这样不会影响单线程主线程在队列中进行事件轮询,执行一些任务。线程池操作完成后,将作为Event生产者,将操作结果放入同一个队列中。简而言之,一个事件轮询EventLoop需要三个组件:EventQueue,属于FIFO模型。一端推送事件数据,另一端拉取事件数据。两端只通过这个队列进行通信,属于异步松耦合。.队列的读轮询线程,事件的消费者,EventLoop的主角。一个单独的线程池,ThreadPool,专门用来执行长任务、重任务、重体力的工作。Node.js也是一个单线程的EventLoop,但是它的运行机制与浏览器环境不同。根据上图,Node.js的运行机制如下。V8引擎解析JavaScript脚本。解析后的代码调用NodeAPI。libuv库负责执行NodeAPI。它将不同的任务分配给不同的线程,形成一个EventLoop(事件循环),并将任务的执行结果以异步的方式返回给V8引擎。然后V8引擎将结果返回给用户。我们可以看到node.js的核心其实就是libuv库。这个库是用c写的,它可以使用多线程技术,而我们的Javascript应用程序是单线程的。Nodejs异步任务执行过程:用户写的代码是单线程的,但是nodejs内部不是单线程的!事件机制:Node.js不使用多线程为每个请求执行工作。相反,它将所有工作添加到一个事件队列中,然后有一个单独的线程来循环遍历队列中的事件。事件循环线程获取事件队列中的顶部条目,执行它,然后获取下一个条目。在执行长时间运行或阻塞的I/O代码时,要注意这一点:它不会被阻塞,它会继续获取下一个事件,而对于阻塞的事件,Node.js会从线程池中取出一个线程来运行这个被阻塞的代码同时将当前事件本身及其回调事件添加到事件队列中(回调嵌套回调)。在Node.js中,由于只有一个单线程不断轮询队列中是否有事件,对于数据库文件系统等I/O操作,包括HTTP请求等容易阻塞等待,如果也是在这个单线程中实现,肯定会阻塞影响其他工作任务的执行。Javascript/Node.js会委托给底层的线程池去执行,会告诉线程池一个回调函数,让单线程继续去执行其他的事情。当这些阻塞操作完成后,将结果连同提供的回调函数一起放入队列中。当单线程不断从队列中读取事件,读取到这些阻塞操作的结果时,就会将这些操作结果作为回调函数的输入参数,进而激活操作Callback。请注意,Node.js的单线程不仅负责读取队列事件,还要执行运行回调函数。这是区别于多线程模式的一个主要特点。在多线程模式下,单线程只负责读取队列。事件不会再做其他事情,会委托其他线程去做其他事情,尤其是在多核的情况下,一个CPU核负责读取队列事件,一个CPU核负责执行激活的任务。这种方法最适合CPU密集型计算。任务。反过来,Node..js的执行激活任务,也就是回调函数中的任务,仍然是在负责轮询的单线程中执行,这就注定了它无法执行CPU繁重的任务,比如将JSON转换为其他数据格式等,这些任务影响事件轮询的效率。6、例子看具体例子:console.log('1')setTimeout(function(){console.log('2')newPromise(function(resolve){console.log('4')resolve()}).then(function(){console.log('5')})setTimeout(()=>{console.log('6')})newPromise(function(resolve){console.log('7')resolve()}).then(function(){console.log('8')})})setTimeout(function(){console.log('9')},0)newPromise(function(resolve){console.log('10')resolve()}).then(function(){console.log('11')})setTimeout(function(){console.log('12')newPromise(function(resolve){console.log('13')resolve()}).then(function(){console.log('14')})})newPromise(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,6chrome运行比较稳定,但是在node环境下运行不稳定。可能有两种情况。chrome运行结果的原因是Promise,process.nextTick()的微任务EventQueue比普通的宏任务EventQueue权限更高。如果在事件队列中取事件时有微任务,则微任务队列中的任务会先执行,除非该任务在下一轮EventLoop中,执行宏任务队列中的任务微任务队列清空后。关于Node中事件循环和异步API的内容,具体讨论可以参考另一篇文章。7.浏览器中的事件循环浏览器中事件循环的执行顺序与Node中的不一致。在浏览器中,我们可以将任务按照性质分为两种,宏任务(macrotask)和微任务(microtask)。macrotask:script(同步代码)、setTimeout、setInterval、setImmediate、MessageChannel、postMessage、I/O、UI渲染microtask:process.nextTick、Promises(这里指浏览器原生实现的Promise)、Object.observe、MutationObserverExecutionsequence:引擎首先从宏任务队列中取出第一个任务。执行完成后,取出微任务队列中的所有任务,依次执行;全部取出;如此循环,直到两个队列中的任务都被取出。网上的帖子大多是深浅不一,甚至有些不一致。以下文章是对学习过程的总结。如果大家发现有什么错误,欢迎留言指出~参考:Node.js说说单线程和异步的EventLoopjs和Nodejs--这次初步学习,彻底理解JavaScript执行机制JavaScript任务队列序列机制(事件循环)PS:欢迎大家关注我的公众号[前-端午茶】加油~另外可以加入“前端下午茶交流群”微信群,长按识别下方二维码加我为好友,加群备注,我会拉你进群~
