前言

js与生俱来的就是单线程无阻塞的脚本语言。 作为单线程语言,js代码执行时都只有一个主线程执行任务。

无阻塞的实现依赖于我们要谈的事件循环。eventloop的规范是真的苦涩难懂,仅仅要理解的话,不推荐去硬啃。

进程与线程

一直在说js是单线程语言。那么什么是线程呢,对于大部分前端同学来说,可能并不是那么清晰。推荐阮大佬的这篇文章,形象生动

首先,计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

进程

进程就好比工厂的车间,它代表CPU所能处理的单个任务。

任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

即资源分配的最小单位,拥有独立的堆栈空间和数据存储空间

线程

线程就好比车间里的工人。车间的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

即程序执行的最小单位,一个进程可以包括多个线程。

相对于进程来说,线程不涉及数据空间的操作,所以切换更高效,开销小。

js单线程的起源

显然多进程可以并行处理,提升cpu的利用率。

但是js初期是作为脚本出现的,其要与DOM进行交互,以完成对用户的展示。

如果多进程,同时操作DOM,那么后果就不可控了。

例如:对于同一个按钮,不同的进程赋予了不同的颜色,到底该怎么展示。

作为一个脚本语言,如果使用多线程+锁的话太多复杂了,所以js就是单线程了。

不过随着js的发展,承载的能力越来越多,局限于单线程使得js的效率等有所限制。

因此增加了web worker来执行非dom的操作。

不过该线程非主线程有一些限制、例如不能操作DOM等,也就是为了保证DOM操作的一致性,这里就先不关注了。

我们主要关注的还是非阻塞的能力基础,即事件循环。

浏览器中的事件循环

说道事件循环就要先说事件队列。

在主线程运行时,会产生堆(heap)和栈(stack)。

堆中存的是我们声明的object类型的数据,栈中存的是基本数据类型以及函数执行时的运行空间。

主线程从任务队列中读取事件,这个过程是循环不断的,所以这种运行机制即Event Loop。


对于同步代码,是直接执行的。
而执行异步方法时同样会加入事件队列中,但是异步事件是有差别的,差别在于执行的优先级不同。

事件分类

因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

  • 以下事件属于宏任务:

setTimeout, setInterval, setImmediate,I/O, UI rendering

  • 以下事件属于微任务

Promise,Object.observe(已废弃),MutationObserver(html5新特性),process.nextTick

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

对于不同类型的任务执行顺序如下:

  1. 同步代码执行
  2. event-loop start
  3. microTasks 队列开始清空(执行)
  4. 检查 Tasks 是否清空,有则跳到 4,无则跳到 6
  5. 从 Tasks 队列抽取一个任务,执行
  6. 检查 microTasks 是否清空,若有则跳到 2,无则跳到 3
  7. 结束 event-loop

大概流程图如下:

不如直接看个栗子:

setTimeout(function () {
console.log(1);
}); new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
// 2 3 1
  1. 区分事件类型:宏任务setTimeout,微任务.then
  2. 同步代码执行 输出2
  3. 微任务队列清空 输出 3
  4. 宏任务执行 输出 1

下面来个稍微复杂的:

setTimeout(()=>{
console.log('A');
},0);
var obj={
func:function () {
setTimeout(function () {
console.log('B')
},0);
return new Promise(function (resolve) {
console.log('C');
resolve();
})
}
};
obj.func().then(function () {
console.log('D')
});
console.log('E');
// c,e,d,b,a

大家可以结合例子自己试下。

node中的事件循环机制

在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。

node中事件循环的实现是依靠的libuv引擎。

我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,

而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。

因此实际上node中的事件循环存在于libuv引擎中。

而node 事件分为下面几大阶段:

  • timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
  • I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
  • idle, prepare: 仅内部使用。
  • poll: 获取新的I/O事件;node会在适当条件下阻塞在这里,等待新的I/O。
  • check: pool阶段之后,执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调

poll阶段

值得额外关注的是poll阶段

该阶段有如下功能:

  1. 执行 timer 阶段到达时间上限的的任务。
  2. 执行 poll 阶段的任务队列。

如果进入 poll 阶段,并且没有 timer 阶段加入的任务,将会发生以下情况

  • 如果 poll 队列不为空的话,会执行 poll 队列直到清空或者系统回调数达到上限
  • 如果 poll 队列为空 ​ 如果设定了 setImmediate 回调,会直接跳到 check 阶段。 如果没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并立即执行。

process.nextTick()

nextTick 比较特殊,它有自己的队列,并且,独立于event loop。 它的执行也非常特殊,无论 event loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。

直接看例子吧:

process.nextTick

process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
}); setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

大概顺序如下:

  1. 因为nextTick的特殊性,当前阶段执行完毕,就执行。所以直接,输出1 2
  2. 执行到timer 输出 TIMEOUT FIRED

setImmediate

setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
}); setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);

这个结果不固定,同一台机器测试结果也有两种:

// TIMEOUT FIRED =>1 =>2
或者
// 1=>TIMEOUT FIRED=>2
  1. 事件队列进入timer,性能好的 小于1ms,则不执行回调继续往下。若此时大于1ms, 则输出 TIMEOUT FIRED 就不输出步骤3了。
  2. poll阶段任务为空,存在setImmediate 直接进入setImmediate 输出1
  3. 然后再次到达timer 输出 TIMEOUT FIRED
  4. 再次进入check 阶段 输出 2

原因在于setTimeout 0 node 中至少为1ms,也就是取决于机器执行至timer时是否到了可执行的时机。

做个对比就比较清楚了:

setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
}); setImmediate(function B(){console.log(4);});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 20);
// 1=>2=>TIMEOUT FIRED

此时间隔时间较长,timer阶段最后才会执行,所以会先执行两次check,出处1,2

下面再看个例子

poll阶段任务队列

var fs = require('fs')

fs.readFile('./yarn.lock', () => {
setImmediate(() => {
console.log('1')
setImmediate(() => {
console.log('2')
})
})
setTimeout(() => {
console.log('TIMEOUT FIRED')
}, 0) })
// 结果确定:
// 输出始终为1=>TIMEOUT FIRED=>2
  1. 读取文件,回调进入poll阶段
  2. 当前无任务队列,直接check 输出1 将setImmediate2加入事件队列
  3. 接着timer阶段,输出TIMEOUT FIRED
  4. 再次check阶段,输出2

小结

浏览器的事件循环

浏览器比较清晰一些,就是固定的流程,当前宏任务结束,就是执行所有微任务(不一定是全部,可能基于系统能力,会有所剩下),然后再下一个宏任务,微任务这样交替进行。

node中的事件循环

主要是把握不同阶段和特殊情况的处理,特别是poll阶段和 process.nextTick任务。

结束语

参考文章:

https://zhuanlan.zhihu.com/p/47152694

https://html.spec.whatwg.org/multipage/webappapis.html#event-loop

http://www.ruanyifeng.com/blog/2014/10/event-loop.html

https://hackernoon.com/understanding-js-the-event-loop-959beae3ac40

https://juejin.im/post/5bac87b6f265da0a906f78d8

感谢上述参考文章,关于事件循环这里就总结完毕了,作为自己的一个学习心得。希望能帮助到有需求的同学,一起进步。

浏览器和Node 中的Event Loop的更多相关文章

  1. 浅析Node.js的Event Loop

    目录 浅析Node.js的Event Loop 引出问题 Node.js的基本架构 Libuv Event Loop Event Loop Phases Overview Poll Phase The ...

  2. 定时器setTimeout()和Node.js的Event Loop

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

  3. [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()

    译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...

  4. 为什么JS是单线程?JS中的Event Loop(事件循环)?JS如何实现异步?setimeout?

    https://segmentfault.com/a/1190000012806637 https://www.jianshu.com/p/93d756db8c81 首先,请牢记2点: (1) JS是 ...

  5. 不要在nodejs中阻塞event loop

    目录 简介 event loop和worker pool event loop和worker pool中的queue 阻塞event loop event loop的时间复杂度 Event Loop中 ...

  6. 浏览器中的 Event Loop

    当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中.一旦执行栈为空, ...

  7. node.js中对Event Loop事件循环的理解

    javascript是单线程的,所以任务的执行都需要排队,任务分为两种,一种是同步任务,一种是异步任务. 同步任务是进入主线程上排队执行的任务,上一个任务执行完了,下一个任务才会执行. 异步任务是不进 ...

  8. 详解JavaScript中的Event Loop(事件循环)机制

    前言 我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言.这是由其最初的用途来决定的:与浏览器交互. 单线程意味着,javascript代码在执行的任何时候,都只有一个主线程 ...

  9. 【Node.js】Event Loop执行顺序详解

    本文基于node 0.10.22版本 关于EventLoop是什么,请看阮老师写的什么是EventLoop 本文讲述的是EventLoop中的执行顺序(着重讲setImmediate, setTime ...

随机推荐

  1. QT运行cmd指令(两种办法:QProcess.start然后waitForFinished,运行cmd /c命令)

    QProcess p(); p.start("route");//写入要运行的指令即可 p.waitForStarted(); p.waitForFinished(); qDebu ...

  2. 知识的内化:学习、实践、输出(与Focus Feedback FixIt的原理是一致的)

    一个人的能力分三个层次: 资源,比如知识.技能.经验.时间.精力.金钱.人脉等 应用流程,即使用资源解决问题的能力,包括做事的方法.流程.策略等,它是你整合应用资源创造价值的能力. 价值取向,即你觉得 ...

  3. 如何在excel中把汉字转换成拼音

    ---恢复内容开始--- 1.启动Excel 2003(其它版本请仿照操作),打开相应的工作表: 2 2.执行“工具→宏→Visual Basic编辑器”命令(或者直接按“Alt+F11”组合键),进 ...

  4. SYN012型B码时统

       SYN012型B码时统 产品概述 SYN012型B码时统是由西安同步电子科技有限公司精心设计.自行研发生产的一款通用性时统终端,内置高精度恒温晶振,接收GPS北斗双模卫星信号,10MHz外部参考 ...

  5. ElasticSearch2.3.1环境搭建哪些不为人知的坑

    首先说明一点,大家最好不要用什么尝鲜版,用比稳定版就好了,要不麻烦不断,另外出了问题,最好去官网,或者google搜索,因为这样靠谱些,要不现在好多都是低版本的,1.4的什么的,结果按照安装,多少情况 ...

  6. Java学习笔记——三层架构

    Layer: UI层: user interface 用户接口层 Biz层:   service business login layer 业务逻辑层 DAO层:   Date Access Obje ...

  7. python面试题(三)列表操作

    接上一篇............. 0x01:列表的去重操作 al = [1, 1, 2, 3, 1, 2, 4] #set方法元素去重 al_set = set(al) print(list(al_ ...

  8. golang开发:类库篇(二) Redis连接池的使用

    为什么要使用连接池 一个数据库服务器只拥有有限的连接资源,一旦所有的连接资源都在使用,那么其它需要连接的资源就只能等待释放连接资源.所以,在连接资源有限的情况下,提高单位时间的连接的使用效率,缩短连接 ...

  9. Python浮点数(小数)运算误差的原因和解决办法

    原因解释:浮点数(小数)在计算机中实际是以二进制存储的,并不精确.比如0.1是十进制,转换为二进制后就是一个无限循环的数:0.0001100110011001100110011001100110011 ...

  10. 利用MAT分析JVM内存问题,从入门到精通(二)

    上一篇文章MAT入门到精通(一)介绍了MAT的使用场景和基本概念,这篇文章开始介绍MAT的基本功能,后面还有两篇,一篇是MAT的高级功能,另一篇是MAT实战案例分析. 三.欢迎页 使用MAT打开一个h ...