实例分析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 相关文章推荐
增强的 JavaScript 的 trim 函数的代码
Aug 13 Javascript
简单通用的JS滑动门代码
Dec 19 Javascript
JavaScript Sort 表格排序
Oct 31 Javascript
Javascript 面向对象特性
Dec 28 Javascript
javascript获取四位数字或者字母的随机数
Jan 09 Javascript
EasyUI中combobox默认值注意事项
Mar 01 Javascript
深入探讨JavaScript String对象
Mar 09 Javascript
浅谈JavaScript中的string拥有方法的原因
Aug 28 Javascript
vue.js指令v-model实现方法
Dec 05 Javascript
JavaScript正则表达式函数总结(常用)
Feb 22 Javascript
详解JavaScript作用域和作用域链
Mar 19 Javascript
详解Vue中使用Axios拦截器
Apr 22 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下将多个数组合并成一个数组的方法与实例代码
2011/02/03 PHP
PHP实现的简单日历类
2014/11/29 PHP
PHP生成plist数据的方法
2015/06/16 PHP
HTML5如何适配 iPhone IOS 底部黑条
2021/03/09 HTML / CSS
抽出www.templatemonster.com的鼠标悬停加载大图模板的代码
2007/07/11 Javascript
Exjs 入门篇
2010/04/07 Javascript
jquery中animate动画积累的解决方法
2013/10/05 Javascript
jquery五角星评分插件示例分享
2014/02/21 Javascript
AngularJS中的包含详细介绍及实现示例
2016/07/28 Javascript
图片上传之FileAPI与NodeJs
2017/01/24 NodeJs
js实现百度登录框鼠标拖拽效果
2017/03/07 Javascript
JavaScript的继承实现小结
2017/05/07 Javascript
ES6使用Set数据结构实现数组的交集、并集、差集功能示例
2017/10/31 Javascript
vue中如何实现后台管理系统的权限控制的方法示例
2018/09/19 Javascript
js实现京东秒杀倒计时功能
2019/01/21 Javascript
微信小程序人脸识别功能代码实例
2019/05/07 Javascript
Python天气预报采集器实现代码(网页爬虫)
2012/10/07 Python
使用 Visual Studio Code(VSCode)搭建简单的Python+Django开发环境的方法步骤
2018/12/17 Python
python计算无向图节点度的实例代码
2019/11/22 Python
Python操作Elasticsearch处理timeout超时
2020/07/17 Python
CSS3实现粒子旋转伸缩加载动画
2016/04/22 HTML / CSS
利用HTML5+CSS3实现3D转换效果实例详解
2017/05/02 HTML / CSS
HTML5 visibilityState属性详细介绍和使用实例
2014/05/03 HTML / CSS
html5开发三八女王节表白神器
2018/03/07 HTML / CSS
DKNY品牌官网:纽约大都会时尚风格
2016/10/20 全球购物
德国化妆品和天然化妆品网上商店:kosmetikfuchs.de
2017/06/09 全球购物
悬挂训练绳:TRX
2017/12/14 全球购物
社区庆中秋节活动方案
2014/02/07 职场文书
质量保证书范本
2014/04/29 职场文书
品质口号大全
2014/06/17 职场文书
2014物价局民主生活会对照检查材料思想汇报
2014/09/24 职场文书
学校组织向国旗敬礼活动方案(中小学适用)
2014/09/27 职场文书
七一晚会主持词
2015/06/29 职场文书
创业计划书之养殖业
2019/10/11 职场文书
手写Spirit防抖函数underscore和节流函数lodash
2022/03/22 Javascript
关于pytest结合csv模块实现csv格式的数据驱动问题
2022/05/30 Python