实例分析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 版本的文本输入框检查器Input Check
Jul 09 Javascript
使用jQuery实现更改默认alert框体
Apr 13 Javascript
CSS3实现动态背景登录框的代码
Jul 28 Javascript
jquery实现的用户注册表单提示操作效果代码分享
Aug 28 Javascript
Highcharts入门之基本属性
Aug 02 Javascript
js实现页面刷新滚动条位置不变
Nov 27 Javascript
详解Jquery Easyui的验证扩展
Jan 09 Javascript
Javascript ES6中数据类型Symbol的使用详解
May 02 Javascript
微信小程序中实现手指缩放图片的示例代码
Mar 13 Javascript
在mpvue框架中使用Vant WeappUI组件库的注意事项【推进】
Jun 09 Javascript
利用不到200行代码写一款属于你自己的js类库
Jul 08 Javascript
js之切换全屏和退出全屏实现代码实例
Sep 09 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+jQuery制作的列表分页的功能模块
2014/10/14 PHP
深入浅析PHP7.0新特征(五大新特征)
2015/10/29 PHP
PHP解压ZIP文件到指定文件夹的方法
2016/11/17 PHP
PHP获取中国时间(上海时区时间)及美国时间的方法
2017/02/23 PHP
phpstudy后门rce批量利用脚本的实现
2019/12/12 PHP
基于jQuery的history历史记录插件
2010/12/11 Javascript
Javascript设置对象的ReadOnly属性(示例代码)
2013/12/25 Javascript
node.js中的path.sep方法使用说明
2014/12/08 Javascript
JS烟花背景效果实现方法
2015/03/03 Javascript
你有必要知道的25个JavaScript面试题
2015/12/29 Javascript
BootStrap制作导航条实例代码
2016/05/06 Javascript
JavaScript类型系统之布尔Boolean类型详解
2016/06/26 Javascript
ng-options和ng-checked在表单中的高级运用(推荐)
2017/01/21 Javascript
详解如何在Vue2中实现组件props双向绑定
2017/03/29 Javascript
easyui-edatagrid.js实现回车键结束编辑功能的实例
2017/04/12 Javascript
AngularJS折叠菜单实现方法示例
2017/05/18 Javascript
es6中的解构赋值、扩展运算符和rest参数使用详解
2017/09/28 Javascript
Vue源码学习之初始化模块init.js解析
2017/11/02 Javascript
element-ui中的select下拉列表设置默认值方法
2018/08/24 Javascript
微信小程序聊天功能的示例代码
2020/01/13 Javascript
[01:38:19]夜魇凡尔赛茶话会 第五期
2021/03/11 DOTA
python中Pycharm 输出中文或打印中文乱码现象的解决办法
2017/06/16 Python
python异常处理和日志处理方式
2019/12/24 Python
Python 格式化输出_String Formatting_控制小数点位数的实例详解
2020/02/04 Python
在PyCharm中实现添加快捷模块
2020/02/12 Python
python使用paramiko实现ssh的功能详解
2020/03/06 Python
python快速安装OpenCV的步骤记录
2021/02/22 Python
用HTML5 Canvas API中的clearRect()方法实现橡皮擦功能
2016/03/15 HTML / CSS
Willer台湾:日本高速巴士/夜行巴士预约
2017/07/09 全球购物
学校四群教育实施方案
2014/06/12 职场文书
教师先进事迹材料
2014/12/16 职场文书
2015庆祝七一建党节94周年活动总结
2015/03/20 职场文书
喜迎建国70周年:有关爱国的名言名句
2019/09/24 职场文书
GO语言异常处理分析 err接口及defer延迟
2022/04/14 Golang
MySQL数据库简介与基本操作
2022/05/30 MySQL
postgresql如何找到表中重复数据的行并删除
2023/05/08 MySQL