浏览器与NodeJS环境 eventloop异同详解(转)(废弃)

原文结论已经废弃,从node11版本开始nodejs的表现和浏览器是相同的,

都是一个宏任务,所有微任务,一个宏任务,所有微任务。

 

总结: 

 

宏任务:setImmediate setTimeout Script I/O UIrendering Script

微任务:Promise.then  

 

在浏览器环境中,一个宏任务,一堆微任务,一个宏任务,一堆微任务

在nodejs环境中也是这样

 

nodejs的 eventloop包括

 

(下面这些都是宏任务)

timers 处理计时器回调 setTimeout setInterval

I/O pending  处理上一次事件循环中未处理完的回调(可忽略)

prepare (可忽略)

polling(执行所有回调,除了timers,setImmediate和close之外的所有回调)

check(执行setImmediate的回调)

close callbacks (处理close回调 如 socket.on('close')

 

区别是 nodejs的event loop会在macrostask之间,执行microtask之前先执行所有的process.nextTick

 

 

------------

 

原文:

结论:浏览器中是一个宏任务,所有微任务,一个宏任务,所有微任务...

          NodeJS中,一种宏任务队列所有任务,所有微任务,一种宏任务队列所有任务,所有微任务...

 

 

 

   ┌───────────────────────┐
┌─>│        timers         │<————— 执行 setTimeout()、setInterval() 的回调
│  └──────────┬────────────┘
|             |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
│  │     pending callbacks │<————— 执行由上一个 Tick 延迟下来的 I/O 回调(待完善,可忽略)
│  └──────────┬────────────┘
|             |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
│  │     idle, prepare     │<————— 内部调用(可忽略)
│  └──────────┬────────────┘     
|             |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │ - (执行几乎所有的回调,除了 close callbacks 以及 timers 调度的回调和 setImmediate() 调度的回调,在恰当的时机将会阻塞在此阶段)
│  │         poll          │<─────┤  connections, │ 
│  └──────────┬────────────┘      │   data, etc.  │ 
│             |                   |               | 
|             |                   └───────────────┘
|             |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
|  ┌──────────┴────────────┐      
│  │        check          │<————— setImmediate() 的回调将会在这个阶段执行
│  └──────────┬────────────┘
|             |<-- 执行所有 Next Tick Queue 以及 MicroTask Queue 的回调
│  ┌──────────┴────────────┐
└──┤    close callbacks    │<————— socket.on('close', ...)
   └───────────────────────┘

 

对比浏览器

想理解整个 loop 的过程,我们可以参照浏览器的 event loop,因为浏览器的比较简单,如下:

   ┌───────────────────────┐
┌─>│        timers         │<————— 执行一个 MacroTask Queue 的回调
│  └──────────┬────────────┘
|             |<-- 执行所有 MicroTask Queue 的回调
| ────────────┘

是不是相比之下非常简洁,就这么两种 task queue,简单的一笔!
用一句话总结浏览器的 event loop 就是:

先执行一个 MacroTask,然后执行所有的 MicroTask;
再执行一个 MacroTask,然后执行所有的 MicroTask;
……
如此反复,无穷无尽……

:可以把 script 标签中的初始同步代码视为一个初始的 MacroTask

解析

其实nodejs与浏览器的区别,就是nodejs的 MacroTask 分好几种,而这好几种又有不同的 task queue,而不同的 task queue 又有顺序区别,而 MicroTask 是穿插在每一种【注意不是每一个!】MacroTask 之间的。

其实图中已经画的很明白:
setTimeout/setInterval 属于 timers 类型;
setImmediate 属于 check 类型;
socket 的 close 事件属于 close callbacks 类型;
其他 MacroTask 都属于 poll 类型。
process.nextTick 本质上属于 MicroTask,但是它先于所有其他 MicroTask 执行;
所有 MicroTask 的执行时机,是不同类型 MacroTask 切换的时候。
idle/prepare 仅供内部调用,我们可以忽略。
pending callbacks 不太常见,我们也可以忽略。

所以我们可以按照浏览器的经验得出一个结论:

先执行所有类型为 timers 的 MacroTask,然后执行所有的 MicroTask(注意 NextTick 要优先哦);
进入 poll 阶段,执行几乎所有 MacroTask,然后执行所有的 MicroTask;
再执行所有类型为 check 的 MacroTask,然后执行所有的 MicroTask;
再执行所有类型为 close callbacks 的 MacroTask,然后执行所有的 MicroTask;
至此,完成一个 Tick,回到 timers 阶段;
……
如此反复,无穷无尽……

为了验证这个结论,我们甚至可以举一个例子:

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

此代码在浏览器环境会输出什么呢?

timer1
promise1
timer2
promise2
 

但是 nodejs 会输出:

timer1
timer2
promise1
promise2

 

如果你已经理解了上面的现象,那我们已经算基本了解 nodejs 的 event loop 了,但是其中还有一点细节

细节一:setTimeout 与 setImmediate 的顺序

本来这不应该成为一个问题,因为在文首显而易见,timers 是在 check 之前的。
但事实上,Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 都写在 Main 进程中,但它们的执行顺序是不确定的:

setTimeout(() => {
  console.log('timeout')
}, 0)

setImmediate(() => {
  console.log('immediate')
})

虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当 Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate

但有一种情况,它们两者的顺序是固定的:

const fs = require('fs')

fs.readFile('test.txt', () => {
  console.log('readFile')
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

和之前情况的区别在于,此时 setTimeout 和 setImmediate 是写在 I/O callbacks 中的,这意味着,我们处于 poll 阶段,然后是 check 阶段,所以这时无论 setTimeout 到期多么迅速,都会先执行 setImmediate。本质上是因为,我们从 poll 阶段开始执行,而非一个 Tick 的初始阶段

细节二:poll 阶段

poll 阶段主要有两个功能:

  • 获取新的 I/O 事件,并执行这些 I/O 的回调,之后适当的条件下 node 将阻塞在这里
  • 当有 immediate 或已超时的 timers,执行它们的回调

poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况:

  1. setImmediate 的 queue 不为空,则进入 check 阶段,然后是 close callbacks 阶段……
  2. setImmediate 的 queue 为空,但是 timers 的 queue 不为空,则直接进入 timers 阶段,然后又来到 poll 阶段……
  3. setImmediate 的 queue 为空,timers 的 queue 也为空,此时会阻塞在这里,因为无事可做,也确实没有循环下去的必要

细节三:关于 pending callbacks 阶段

在很多文章中,将 pending callbacks 阶段都写作 I/O callbacks 阶段,并说在此阶段,执行了除 close callbacks、 timers、setImmediate以外的几乎所有的回调,也就是把 poll 阶段的工作与此阶段的工作混淆了。
在我阅读时,就曾产生过疑问,假如大部分回调是在 I/O callbacks 阶段执行的,那么 poll 阶段就没有理由阻塞,因为你并不能保证“无事可做”,你得去 I/O callbacks 阶段检查一下才知道嘛!
所以最终结合其他几篇文章以及对源码的分析,应该可以确定,I/O callbacks 更准确的叫做 pending callbacks,它所执行的回调是比较特殊的、且不需要关心的,而真正重要的、大部分回调所执行的阶段是在 poll 阶段。

关于 pending callbacks 有如下说法,可以作为参考

查阅了libuv 的文档后发现,在 libuv 的 event loop 中,I/O callbacks 阶段会执行 Pending callbacks。绝大多数情况下,在 poll 阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在 I/O callbacks 阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。

严格来说,i/o callbacks并不是处理文件i/o的callback 而是处理一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误callback。参考 因为,pending_queue的入列(queue_insert_tail)是通过一个叫 uv__io_feed 的api来调用的 而 uv__io_feed API是在tcp/udp/stream/pipe等相关API调用


参考资料1
参考资料2
 
转载自 https://www.jianshu.com/p/deedcbf68880
posted @ 2019-05-13 03:40  hh9515  阅读(668)  评论(0编辑  收藏  举报