天天看点

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

EventLoop是什么

EventLoop是一个代码执行模型,它规定了代码的执行顺序,nodeJs和JavaScript拥有各自的EventLoop,理解EventLoop能让我们更加理解代码的执行,更好地掌控代码

宏任务和微任务

​ JavaScript中的代码执行分为同步执行和异步执行,而异步执行的代码中又分为宏任务和微任务,它们的执行顺序有所不同。它们并没有什么概念性的差别,只是根据执行顺序的不同而进行区分,仅此而已。因此,只需要硬性的记住哪些是宏任务哪些是微任务就可以了。

宏任务(macrotask/task):

执行顺序为同步和微任务之后,宏任务具体的种类有:

  • setTimeout
  • setInterval
  • setImmediate (Node)
  • requestAnimationFrame (浏览器)
  • I/O
  • UI rendering (浏览器独有)

微任务(microtask/jobs):

执行顺序为同步之后,宏任务之前,具体的种类有:

  • process.nextTick (Node)
  • Promise的then和catch中的代码
  • Object.observe(已废弃)
  • MutationObserver

javaScript EventLoop

简单理解的讲解

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

​ 在JavaScript中,代码分为同步代码和异步代码,异步代码又分为宏任务和微任务。在eventLoop模型中,规定了这些代码的执行顺序:

  1. 首先按照顺序执行所有的同步代码
  2. 执行同步代码碰到了微任务,将其按顺序从后按照队列的形式放入当前执行栈中待执行(参考执行栈1的结构)
  3. 执行同步代码碰到了宏任务,将其按顺序从前按照队列的形式放入下一个执行栈中待执行
  4. 所有同步代码执行完毕,开始按顺序执行微任务
  5. 所有微任务执行完毕,开启下一个执行栈按顺序执行宏任务
  6. 宏任务执行中碰到了微任务,将微任务按顺序从后按照队列的形式放入当前执行栈中待执行(参考执行栈2的结构)
  7. 执行完当前宏任务后执行所有新的微任务
  8. 新的微任务执行完毕,开启下一个执行栈继续执行宏任务(参考执行栈3的结构)
  9. 反复执行6-8的步骤直到宏任务全部完成

由上面的步骤可见,在执行完同步代码后,代码的执行步骤形成了一个循环圈,这个循环圈就是所谓的EventLoop

实际上真实运行的标准讲解

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

真实的eventLoop模型的运行,执行结构分为3部分:主执行栈Stack,宏任务队列Task Queue,微任务队列Microtask Queue。代码的执行顺序为:

  1. 所有的同步代码按顺序放入主执行栈中执行
  2. 执行同步代码的时候碰到了微任务,将微任务放入微任务队列中
  3. 执行同步代码的时候碰到了宏任务,将宏任务放入宏任务队列中
  4. 所有同步代码执行完毕,从微任务队列中按顺序取出微任务放入主执行栈中依次执行,直到微任务队列为空
  5. 所有微任务执行完毕,从宏任务队列中取出队首的宏任务放入主执行栈中执行
  6. 如果在执行宏任务的时候碰到了微任务则将其加入微任务队列中
  7. 当前宏任务执行完毕,查看微任务队列中是否有新增的微任务,如果有则按顺序取出放入主执行栈中执行,如果没有则从宏任务队列中取出下一个宏任务放入主执行栈中执行
  8. 反复执行 5-8直到完成所有宏任务

由上面的步骤可见,在执行完同步代码后,代码的执行步骤形成了一个循环圈,这个循环圈就是所谓的EventLoop

NodeJs EventLoop

先附上官方文档连接

https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

我自己通读完全原文后差不多是可以全理解了eventloop的运行和其中一些api的差别了,讲的非常的详细

然后附上讲解

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

​ NodeJs中的 EventLoop 与 浏览器JavaScript的EventLoop相比,大体上相同,细看完全不同,宏观上来说,都是先执行完同步代码后执行异步代码,先执行微任务后执行宏任务的这么一个循环

​ 但是放大来看,在具体的运行之中它们在循环方面其实并不相同:

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

           

这里是官方给出的标准NodeJs EventLoop运行模型。这里先对EventLoop的不同层级进行讲解:

  • timer:存放所有

    setTimeout

    setInterval

    中定义的回调函数的队列,需要注意的是,这里执行的计时器和间隔器的回调并不是严格按照它设定的时间来的,一般来说它会受到poll阶段运行的影响
  • pending callbacks:存放执行一些系统操作的回调的队列,一些错误报告之类的都会在这里进行执行报告
  • idle, prepare:仅供node内部使用
  • poll:I/O操作回调函数存放队列
  • check:

    setImmediate()

    函数定义的回调存放队列
  • close callbacks:存放与socket的关闭(close)和摧毁(destroy)有关的回调的队列

​ 以上的每一个队列都代表EventLoop的一个阶段,而每个队列里的回调任务都代表宏任务,且每个队列都具有执行上的最大长度(node为了防止eventLoop一直卡在某一个阶段而设置的执行上限,防止把别的阶段特别是I/O饿死)。

​ NodeJs我看他官方文档上来说是没有提到微任务这个点的,但是它讲了一些别的东西,那些东西的执行特征和微任务的执行特征基本一致,而且还有一个Promise的微任务(虽然我在Node中从没有单独用过,但是axios是可以在服务端发出请求的,而axios是基于promise的),因此我就着我读过文档后的理解(可能会出错,毕竟只全英原文读了3遍,加上个人经验可能并不是很足,如果出错了还请告诉我)来给出一个微任务的队列结构

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

​ 同浏览器的JavaScript一般,Node的微任务同样是在每一次宏任务执行完毕之后都会进行一次执行,放在上图的结构中就是在每一个EventLoop的阶段执行完毕后,下一个阶段开始之前都会对微任务的队列进行一次清空性的执行,其中优先执行process.nextTick所放入的回调函数。

将各方面简单介绍过后,以下将对NodeJs的EventLoop循环进行解释:

  1. 执行所有的同步代码
  2. 进入timers阶段,查看timers队列中是否有注册的回调,如果有则清空性执行
  3. 清空性执行微任务队列
  4. 进入pending阶段,查看是否有错误报告回调注册,如果有则清空性执行
  5. 清空性执行微任务队列
  6. 进入prepare阶段,仅供node内部使用,不开展讨论
  7. 清空性执行微任务队列
  8. 进入poll阶段,执行所有注册了的I/O回调,具体的在下文单独叙述
  9. 清空性执行微任务队列
  10. 进入check阶段,清空性执行

    setImmediate

    注册的回调
  11. 清空性执行微任务队列
  12. 进入close阶段,查看是否有socket的关闭(close)或摧毁(destroy)事件触发,有则执行
  13. 清空性执行微任务队列
  14. 反复执行2-13

以上的执行循环就是整个NodeJs的EventLoop

EventLoop poll 阶段

​ 在node中的EventLoop,这个阶段的执行是相对复杂的,且这个阶段的执行一定程度上影响着别的阶段的执行(因为node有时候会自动阻塞在这里)。这个阶段主要是执行I/O操作的回调,当EventLoop来到这个阶段且timer队列是空的情况,会发生如下几种情况:

  • 如果poll队列是非空的情况,会正常清空性执行所有队列中的回调函数直到队列为空或者达到执行上限,之后EventLoop向下循环
  • 如果poll队列是空的情况,又会发生两种情况:
    • 如果check队列是非空状态,则node会立即终止poll阶段,EventLoop正常向下循环运转
    • 如果check队列是空的,且check和timer队列没有回调任务,node将在此处阻塞,等待I/O操作完成,并立即执行所有新添加的I/O回调

​ node在此阶段的阻塞并不影响EventLoop的运行,因为每次poll队列为空的时候node都会检查timer队列是否有新的回调任务,如果有新的回调任务EventLoop会正常向下循环,保证了timer回调的执行。

timer阶段一定程度上受poll阶段影响

​ 在timer的注册中,回调任务的执行会有一定的间隔,假设设置了一个setTimeout,100ms后执行回调,node并不会傻傻的在这里等,此时它会检查poll队列是否有能在100ms完成的事(即EventLoop向下循环),假设有个poll中有个I/O执行出结果用了95ms,然后回调执行会用10ms,此时95ms小于100ms,所以node会果断的去执行poll中的I/O回调,执行完后才执行timer注册的回调(即EventLoop再向下循环回到timer),但是此时时间已经过去了105ms,因此timer的回调执行并不是严格按照设定的时间间隔执行,会有一定的偏差。

​ 如果你已经理解了上面的poll阶段解释,这里就不难理解。

NodeJs EventLoop与JavaScript EventLoop详解EventLoop是什么宏任务和微任务javaScript EventLoopNodeJs EventLoop

Node中的setTimeout、setInterval和setImmediate

setTimeout/setInterval和setImmediate在node中的使用是有区别的:

  • setTimeout与setInterval的回调函数会放在timer队列中执行
  • setImmediate的回调会放在check队列中执行

虽然话是这么说,但是它们的运行顺序在不同情况下是不同的,分为2种情况:

两者都在主模块且其中没有涉及到I/O操作的时候,它们的运行先后顺序是不确定的,谁先运行谁后运行很大程度上依赖于当前EventLoop所处的阶段。以下举个例子:

  • 一种情况:你在Poll阶段分别注册了setTimeout和setImmediate,那么根据EventLoop的运行顺序就是先check阶段后timer阶段,即先setImmediate后setTimeout/setInterval
  • 另一种情况:你在close阶段注册了setTimeout和setImmediate,那么根据EventLoop的运行顺序就是先timer阶段后check阶段,即先setTimeout/setInterval后setImmediate

但是,一旦其中涉及到了I/O操作,不管你用setTimeout/setInterval向timer中注册了多少回调,setImmediate总是会比setTimeout/setInterval先执行

setImmediate和process.nextTick

​ 在node中,process.nextTick允许你主动地添加一个微异步回调,每个EventLoop阶段末尾会清空性执行微任务队列。

​ 一般来说,使用它的情景有:

  1. 需要手动报错(虽然这个错误不影响下面代码执行,但是必须要报告)的时候
  2. 需要清理一些不需要的资源的时候
  3. 需要在下一个阶段之前尝试重新发送请求的时候
  4. 有函数需要使用微异步解决的时候(也就是同步解决不了问题,但是它一定要在下个阶段前执行的时候)

​ 除了以上场景之外都不推荐使用它,因为使用它是有风险的,需要谨慎使用,一旦你递归使用process.nextTick,就会导致EventLoop迟迟无法运行到下一个阶段,这会导致你的I/O操作饿死,而且相对于setImmediate来说,它的执行不是那么好掌控。因此大部分情况都推荐使用setImmediate而非process.nextTick。

继续阅读