实例分析JS与Node.js中的事件循环


Posted in Javascript onDecember 12, 2017

这两天跟同事同事讨论遇到的一个问题,js中的event loop,引出了chrome与node中运行具有setTimeoutPromise的程序时候执行结果不一样的问题,从而引出了Nodejs的event loop机制,记录一下,感觉还是蛮有收获的

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执行的结果不一样,这就很有意思了。

1. JS 中的任务队列

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

2. 任务队列 event loop

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。主线程不断重复上面的第三步。

实例分析JS与Node.js中的事件循环

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

3. 定时器 setTimeoutsetInterval

定时器功能主要由setTimeout()setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

4. Node.js的Event Loop

事件轮询主要是针对事件队列进行轮询,事件生产者将事件排队放入队列中,队列另外一端有一个线程称为事件消费者会不断查询队列中是否有事件,如果有事件,就立即会执行,为了防止执行过程中有堵塞操作影响当前线程读取队列,事件消费者线程会委托一个线程池专门执行这些堵塞操作。

实例分析JS与Node.js中的事件循环

Javascript前端和Node.js的机制类似这个事件轮询模型,有的人认为Node.js是单线程,也就是事件消费者是单线程不断轮询,如果有堵塞操作怎么办,不是堵塞了当前单线程的执行吗?

其实Node.js底层也有一个线程池,线程池专门用来执行各种堵塞操作,这样不会影响单线程这个主线程进行队列中事件轮询和一些任务执行,线程池操作完以后,又会作为事件生产者将操作结果放入同一个队列中。

总之,一个事件轮询Event Loop需要三个组件:

事件队列Event Queue,属于FIFO模型,一端推入事件数据,另外一端拉出事件数据,两端只通过这个队列通讯,属于一种异步的松耦合。队列的读取轮询线程,事件的消费者,Event Loop的主角。单独线程池Thread Pool,专门用来执行长任务,重任务,干繁重体力活的。

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

实例分析JS与Node.js中的事件循环

根据上图,Node.js的运行机制如下。

V8引擎解析JavaScript脚本。解析后的代码,调用Node API。 libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。 V8引擎再将结果返回给用户。

我们可以看到node.js的核心实际上是libuv这个库。这个库是c写的,它可以使用多线程技术,而我们的Javascript应用是单线程的。

Nodejs 的异步任务执行流程:

实例分析JS与Node.js中的事件循环

用户写的代码是单线程的,但nodejs内部并不是单线程!

事件机制:

Node.js不是用多个线程为每个请求执行工作的,相反而是它把所有工作添加到一个事件队列中,然后有一个单独线程,来循环提取队列中的事件。事件循环线程抓取事件队列中最上面的条目,执行它,然后抓取下一个条目。当执行长期运行或有阻塞I/O的代码时

在Node.js中,因为只有一个单线程不断地轮询队列中是否有事件,对于数据库文件系统等I/O操作,包括HTTP请求等等这些容易堵塞等待的操作,如果也是在这个单线程中实现,肯定会堵塞影响其他工作任务的执行,Javascript/Node.js会委托给底层的线程池执行,并会告诉线程池一个回调函数,这样单线程继续执行其他事情,当这些堵塞操作完成后,其结果与提供的回调函数一起再放入队列中,当单线程从队列中不断读取事件,读取到这些堵塞的操作结果后,会将这些操作结果作为回调函数的输入参数,然后激活运行回调函数。

请注意,Node.js的这个单线程不只是负责读取队列事件,还会执行运行回调函数,这是它区别于多线程模式的一个主要特点,多线程模式下,单线程只负责读取队列事件,不再做其他事情,会委托其他线程做其他事情,特别是多核的情况下,一个CPU核负责读取队列事件,一个CPU核负责执行激活的任务,这种方式最适合很耗费CPU计算的任务。反过来,Node..js的执行激活任务也就是回调函数中的任务还是在负责轮询的单线程中执行,这就注定了它不能执行CPU繁重的任务,比如JSON转换为其他数据格式等等,这些任务会影响事件轮询的效率。

5. Nodejs特点

实例分析JS与Node.js中的事件循环

NodeJS的显著特点:异步机制、事件驱动。

事件轮询的整个过程没有阻塞新用户的连接,也不需要维护连接。基于这样的机制,理论上陆续有用户请求连接,NodeJS都可以进行响应,因此NodeJS能支持比Java、php程序更高的并发量。

虽然维护事件队列也需要成本,再由于NodeJS是单线程,事件队列越长,得到响应的时间就越长,并发量上去还是会力不从心。

RESTful API是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。

6. 实例

看一个具体实例:

console.log('1')
setTimeout(function() {
 console.log('2')
 new Promise(function(resolve) {
 console.log('4')
 resolve()
 }).then(function() {
 console.log('5')
 })
 setTimeout(() => {
 console.log('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha

chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种情况。

chrome运行的结果的原因是Promiseprocess.nextTick()的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,如果取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了之后再执行宏任务队列里的任务。

Javascript 相关文章推荐
jquery ui dialog里调用datepicker的问题
Aug 06 Javascript
window.event快达到全浏览器支持了,以后使用就方便了
Nov 30 Javascript
Javscript调用iframe框架页面中函数的方法
Nov 01 Javascript
JavaScript组合拼接字符串的效率对比测试
Nov 06 Javascript
pace.js页面加载进度条插件
Sep 29 Javascript
jQuery使用Selectator插件实现多选下拉列表过滤框(附源码下载)
Apr 08 Javascript
JS数组排序方法实例分析
Dec 16 Javascript
javascript 玩转Date对象(实例讲解)
Jul 11 Javascript
JavaScript引用类型Array实例分析
Jul 24 Javascript
JS中数据结构之栈
Jan 01 Javascript
layui lay-verify form表单自定义验证规则详解
Sep 18 Javascript
vue等两个接口都返回结果再执行下一步的实例
Sep 08 Javascript
Vue2.0学习之详解Vue 组件及父子组件通信
Dec 12 #Javascript
JS中精巧的自动柯里化实现方法
Dec 12 #Javascript
Vue2.0 slot分发内容与props验证的方法
Dec 12 #Javascript
分析JS中this引发的bug
Dec 12 #Javascript
微信小程序使用progress组件实现显示进度功能【附源码下载】
Dec 12 #Javascript
基于input动态模糊查询的实现方法
Dec 12 #Javascript
详解vue.js之props传递参数
Dec 12 #Javascript
You might like
开发大型 PHP 项目的方法
2007/01/02 PHP
PHP sprintf()函数用例解析
2011/05/18 PHP
php实现的RSS生成类实例
2015/04/23 PHP
如何用PHP做到页面注册审核
2017/03/02 PHP
php实现微信发红包功能
2018/07/13 PHP
javascript 实现父窗口引用弹出窗口的值的脚本
2007/08/07 Javascript
15 个 JavaScript Web UI 库
2010/05/19 Javascript
ExtJS中设置下拉列表框不可编辑的方法
2014/05/07 Javascript
js类式继承与原型式继承详解
2016/04/07 Javascript
jquery模拟多级复选框效果的简单实例
2016/06/08 Javascript
Javascript获取图片原始宽度和高度的方法详解
2016/09/20 Javascript
VUEJS 2.0 子组件访问/调用父组件的实例
2018/02/10 Javascript
vue2.0 资源文件assets和static的区别详解
2018/04/08 Javascript
Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码
2018/05/21 Javascript
js继承的这6种方式!(上)
2019/04/23 Javascript
Python enumerate遍历数组示例应用
2008/09/06 Python
python求crc32值的方法
2014/10/05 Python
Python while 循环使用的简单实例
2016/06/08 Python
python求解数组中两个字符串的最小距离
2018/09/27 Python
Python3实现对列表按元组指定列进行排序的方法分析
2018/12/22 Python
Python JSON格式数据的提取和保存的实现
2019/03/22 Python
python线程中的同步问题及解决方法
2019/08/29 Python
通过celery异步处理一个查询任务的完整代码
2019/11/19 Python
Python 根据数据模板创建shapefile的实现
2019/11/26 Python
python函数定义和调用过程详解
2020/02/09 Python
python3将变量写入SQL语句的实现方式
2020/03/02 Python
Pytest单元测试框架如何实现参数化
2020/09/05 Python
AE美国鹰日本官方网站: American Eagle Outfitters
2016/12/10 全球购物
巴基斯坦购物网站:Goto
2019/03/11 全球购物
公司出纳岗位职责
2013/12/07 职场文书
《在山的那边》教学反思
2014/02/23 职场文书
文艺晚会主持词
2014/03/24 职场文书
2015社区健康教育工作总结
2015/05/20 职场文书
2016年学校“6﹒26国际禁毒日”宣传活动总结
2016/04/05 职场文书
Java实现简单小画板
2022/06/10 Java/Android
Python中np.random.randint()参数详解及用法实例
2022/09/23 Python