JS中比较让人头疼的问题之一要算异步事件了,比如我们经常要等后台返回数据后进行dom操作,又比如我们要设置一个定时器完成特定的要求。在这些同步与异步事件里,异步事件肯定是在同步事件之后的,但是异步事件之间又是怎么样的一个顺序呢,比如多个setTimeout事件又是怎么样一个执行顺序?这就涉及到事件循环:Event Loop。

JS的单线程

虽然现在的JS可以用来做多方面的开发,但是最初的JS是浏览器的专用语言,用来操作DOM。所以从诞生之初,JS就被设计成单线程语言,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

但是这种单线程机制却制造了另一个麻烦,假如一个操作需花费很长时间,那么此时浏览器就会一直等待这个操作完成,就会造成不好的体验。因此,JS的另一个事件就是异步事件。异步事件是专门将一些事件以队列的形式储存到浏览器的任务队列中,等同步事件执行完后再去执行,这样就避免了页面堵塞。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

浏览器的事件循环

如上图所示,js中的基本数据与对象都会储存在栈内存中,其中复杂类型数据对象会在堆内存储存其数据结构,栈内存储存的是对这个数据结构的引用。

执行栈

javaScript是单线程,也就是说只有一个主线程,主线程有一个栈。当JS代码执行时,代码会被推入执行栈中进行运行,运行代码的过程中,同步事件会立即执行,其中Dom、Ajax以及SetTimeout等异步事件会注册回调函数,放入事件回调队列中,等同步代码执行完之后执行。这样一个循环便是浏览器的Event Loop。

异步过程

但是在回调队列中这些事件又是怎么样一个执行顺序呢?实际上异步队列存在两个队列,一个宏任务队列,一个微任务队列,其这就涉及到两个概念:

  • 宏任务(MacroTask):
    包括整体代码script,setTimeout、setInterval、setImmediate、I/O、UI渲染
  • 微任务(MicroTask):
    Promise、process.nextTick、Object.observe、MutationObserver

在栈内存中代码执行完后,浏览器空闲,立即处理回调队列,将回调队列中的宏任务队列中的事件推入执行栈中执行。

  • 首先会执行宏任务,如果宏任务中存在宏任务,则会把该任务放到宏任务队列中。如果该任务里存在微任务,则把微任务放在微任务队列。
  • 在这个宏任务执行完后,首先去看微任务队列中是否有任务,然后把微任务推到执行栈中执行。

执行完微任务队列,这一次循环就结束了,然后再进行在宏任务队列中进行下一个宏任务,微任务,直至回调队列清空。

上述事件归纳后,以下例说明:

分析:

循环1:

  • 【task队列:script ;microtask队列:】
    1.首先整个代码被推到执行栈中执行,这是一个宏任务(整个script代码)
    2.运行中,同步代码立即执行,new Promise中的fn是立即执行的。setTimeout被放在宏任务队列中,promise1、promise2被放在微任务队列中。
  • 【task队列:setTimeout ;microtask队列:promise1、promise2】
    3.宏任务script执行完后,执行微任务队列,取出microtask队列,推入执行栈执行,第一次循环到此结束。

循环2:

  • 【task队列:setTimeout ;microtask队列:】
    4.取出宏任务中的setTimeout推入执行栈执行,如果有微任务则,则被放在微任务队列(这里没有)。
    5.宏任务执行完,去微任务队列执行(微任务队列为空)。
  • 【task队列:;microtask队列:】
    6.宏任务队列为空,循环至此结束。

Nodejs 事件循环

nodejs中的事件循环跟浏览器不一样,浏览器的循环是遵循ES标准里的,nodejs里的循环是通过LIBUV库实现的。

当 Node.js 启动时,会做这几件事

  • 初始化 event loop
  • 开始执行脚本(或者进入 REPL,本文不涉及 REPL)。这些脚本有可能会调用一些异步 API、设定计时器或者调用 process.nextTick()
  • 开始处理 event loop

nodejs的Event Loop 一共有6个阶段:

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
  1. ┌───────────────────────┐
    ┌─>│ timers
    └──────────┬────────────┘
    ┌──────────┴────────────┐
    I/O callbacks
    └──────────┬────────────┘
    ┌──────────┴────────────┐
    idle, prepare
    └──────────┬────────────┘ ┌───────────────┐
    ┌──────────┴────────────┐ incoming:
    poll │<─────┤ connections,
    └──────────┬────────────┘ data, etc.
    ┌──────────┴────────────┐ └───────────────┘
    check
    └──────────┬────────────┘
    ┌──────────┴────────────┐
    └──┤ close callbacks
    └───────────────────────┘

大专栏  Event Loop事件循环,GET!d>

其中我们主要需要关注的是timers、poll、check阶段:

  • timers 阶段:这个阶段执行 setTimeout 和 setInterval 的回调函数。
  • I/O callbacks 阶段:不在 timers 阶段、close callbacks 阶段和 check 阶段这三个阶段执行的回调,都由此阶段负责,这几乎包含了所有回调函数。
  • idle, prepare 阶段(译注:看起来是两个阶段,不过这不重要):event loop 内部使用的阶段(译注:我们不用关心这个阶段)
  • poll 阶段:获取新的 I/O 事件。在某些场景下 Node.js 会阻塞在这个阶段。
  • check 阶段:执行 setImmediate() 的回调函数。
  • close callbacks 阶段:执行关闭事件的回调函数,如 socket.on(‘close’, fn) 里的 fn。

timers阶段

计时器实际上是在指定多久以后可以执行某个回调函数,而不是指定某个函数的确切执行时间。当指定的时间达到后,计时器的回调函数会尽早被执行。如果操作系统很忙,或者 Node.js 正在执行一个耗时的函数,那么计时器的回调函数就会被推迟执行。

poll 阶段(轮询阶段)

poll 阶段有两个功能:

  1. 如果发现计时器的时间到了,就绕回到 timers 阶段执行计时器的回调。
  2. 然后再,执行 poll 队列里的回调。

当 event loop 进入 poll 阶段,如果发现没有计时器,就会:

  • 如果 poll 队列不是空的,event loop 就会依次执行队列里的回调函数,直到队列被清空或者到达 poll 阶段的时间上限。
  • 如果 poll 队列是空的,就会:
    1. 如果有 setImmediate() 任务,event loop 就结束 poll 阶段去往 check 阶段。
    2. 如果没有 setImmediate() 任务,event loop 就会等待新的回调函数进入 poll 队列,并立即执行它。

一旦 poll 队列为空,event loop 就会检查计时器有没有到期,如果有计时器到期了,event loop 就会回到 timers 阶段执行计时器的回调。

check 阶段

这个阶段允许开发者在 poll 阶段结束后立即执行一些函数。如果 poll 阶段空闲了,同时存在 setImmediate() 任务,event loop 就会进入 check 阶段,执行setImmediate() 回调。

Event Loop 大体流程

每一个阶段都有一个队列,我们只关注 timers、poll、check阶段来分析一下,我们在用命令行运行 node server.js 命令时,发生了什么:
1、Node.js启动,初始化 Event Loop
2、运行server.js脚本内容
3、开始运行Event Loop
4、timers阶段看脚本里是否设置定时器setTimeout,比如一个4ms延迟与一个100ms延迟的定时器,把它放到timers队列中,进入下一步,I/O callbacks 阶段,idle, prepare 阶段,这两个阶段都不会停留。
5、进入poll(轮询)阶段,首先它会查看定时器时间是否到了,比如4ms到了,他就进入下一阶段check阶段、close callbacks 阶段,然后回到timers阶段执行设置的4ms回调函数,接着继续第4步到第5步。4ms没到,则停留在这一阶段,处理poll队列里的任务,直到4ms到、100ms到,然后循环回到timers阶段执行回调。

这里有一个问题:当poll阶段在处理任务1时,比如这个任务1要花费100ms,在这100ms期间,setTimeout定时器到了,则它的回调会等poll处理玩任务1后立即循环进入timers阶段执行

6、从poll阶段进入check阶段时,主要是看是否有setImmediate() 任务,如果有则立即执行,然后再进入close callbacks 阶段,进行循环,进入timers阶段。

setImmediate() vs setTimeout()

setImmediate 和 setTimeout 很相似,但是其回调函数的调用时机却不一样。

setImmediate() 的作用是在当前 poll 阶段结束后调用一个函数。 setTimeout() 的作用是在一段时间后调用一个函数。一般来说 setImmediate 会先于 setTimeout 执行,但是第一次启动的时候不一样,这两者的回调的执行顺序取决于 setTimeout 和 setImmediate 被调用时的环境。
例如:

  1. 1
    2
    3
    4
    5
    6
    7
  1. setTimeout(()=>{
    console.log('setTiomeout')
    },0)
  2.  
  3. setInmediate(()=>{
    console.log('setInmediate')
    })

为什么会发生这种情况呢?因为我们启动node.js时, node会做三件事, 初始化event loop,运行脚本,开始event loop。运行脚本与开始event loop这两件事不是同时执行的,它两中间间隔多少并不清楚,这跟环境性能有关。然后要注意的一点,setTimeout的延迟时间最小为4ms,所以这里的0相当于4。

  • 可能两者间隔5ms,当进入timers阶段的时候,node发现,4ms已经过了,立即执行setTimeout定时器回调,然后执行setImmediate。
  • 也可能两者间隔3ms,当进入timers阶段的时候,node发现,4ms还没过,就进入下一阶段,一直到checked,执行setImmediate,然后等到4ms时再执行setTimeout。

process.nextTick()

从技术上来讲 process.nextTick() 并不是 event loop 的一部分。实际上,event loop 再次进入循环前,会去先执行process.nextTick()。

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
  1. setTimeout(()=>{
    console.log('setTiomeout')
    },0)
  2.  
  3. setInmediate(()=>{
    console.log('setInmediate')
    })
  4.  
  5. proces.nextTick(()=>{
    console.log('nextTick')
    })

上述代码中nextTick先于其它两个执行,Vue中有Vue.nextTick()方法就是类似的思想。

注:
本篇文章参考:
Event Loop、计时器、nextTick
这一次,彻底弄懂 JavaScript 执行机制
从event loop规范探究javaScript异步及浏览器更新渲染时机

Event Loop事件循环,GET!的更多相关文章

  1. javascript的event loop事件循环

    javascript的event loop事件循环 这是今天一个朋友发给我的一个面试题, 感觉还挺有意思的, 写个博客以供分享 先看看这个面试题目: 观察下面的代码,写出输出结果 console.lo ...

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

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

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

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

  4. 浅谈 Event loop (事件循环)

    从Event Loop谈JS的运行机制 先来理解一个概念: JS分为同步任务和异步任务 同步任务都在主线程上执行,形成一个执行栈 Execute Content Stack 主线程之外,事件触发线程管 ...

  5. 进程,线程,Event Loop(事件循环),Web Worker

    线程,是程序执行流的最小单位.线程可与同属一个进程的其他线程共享所拥有的全部资源,同一进程中的多个线程之间可以并发执行.线程有就绪,阻塞,运行三种基本状态. 阮一峰大神针对进程和线程的类比,很是形象: ...

  6. js event loop事件循环

    浏览器环境 以下两段代码是等价的.req对事件的回调设置,实际上就是当前主线程任务队列的任务. var req = new XMLHttpRequest(); req.open('GET', url) ...

  7. JavaScript event loop事件循环 macrotask与microtask

    macrotask  姑且称为宏任务,在很多上下文也被简称为task.例如: setTimeout, setInterval, setImmediate, I/O, UI rendering. mic ...

  8. Evevt Loop 事件循环

    目录 JavaScript 是一门单线程的语言 一.什么是event Loop的执行机制 练习 异步任务-setTimeout 练习1: 练习2: 练习3: 练习4: 二 事件队列作用 同步任务 例1 ...

  9. JavaScript Concurrency model and Event Loop 并发模型和事件循环机制

    原文地址:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop JavaScript 有一个基于 event loop 的 ...

随机推荐

  1. 多标签图像分类任务的评价方法-mAP

    http://blog.sina.com.cn/s/blog_9db078090102whzw.html 多标签图像分类(Multi-label Image Classification)任务中图片的 ...

  2. ZEOSDBO控件的安装及使用方法

    步骤:1:下载最新版的ZEOSDBO,官网:http://sourceforge.net/projects/zeoslib/ 2:解压文件到文件安装目录下:C:\Program Files\Embar ...

  3. C/C++ memcpy函数的用法

    功能 memcpy指的是c和c++使用的内存拷贝函数,memcpy函数的功能是从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中 头文件 所在头文件 <s ...

  4. rsync+nfs+sersync实战案例

    回顾: 1.rsync 统一备份各个服务器的配置文件或重要文件 系统配置文件 日志文件 系统日志文件 messages.secure.cron 服务日志文件 access_log.access.log ...

  5. vim的背景配置

    1.vim背景颜色的配置 在usr/share/vim/vim74/colors   目录下有可以配置的颜色方案,可以查看: 命令:vim/etc/vimrc 打开vim的配置文件,   在最后加入一 ...

  6. Zabbix常用监控项整理

    zabbix常用key:http://blog.51cto.com/ttxsgoto/1771752 linux主机cpu使用率超过90%的时候报警:https://blog.csdn.net/reb ...

  7. Linux基础篇九:用户管理

    查看当前用户的ID信息(也可以查看其他用户的ID信息) 每个进程都会有一个用户身份运行 cat /etc/passwd 账号的操作: useradd  (新建用户) 例题:   groupadd  s ...

  8. mysql绿色版安装及授权连接

    --安装包官网下载地址:https://dev.mysql.com/downloads/mysql/5.7.html#downloads --安装教程参见:https://www.cnblogs.co ...

  9. Git ubuntu 升级

    外文文档 This team will distribute the most current stable package of Git for Ubuntu. Stable releases: h ...

  10. java replaceall 用法:处理特殊字符

    public class TryDotRegEx { public static void main(String[] args) { // TODO Auto-generated method st ...