Node的事件循环

1.同步任务与异步任务
(1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务;
(2)异步任务:不进入主线程而是进入任务队列的任务,只有等主线程的任务执行完毕后,任务队列开始通知主线程,请求将异步任务进入到主线程执行;

2.浏览器环境与node环境的事件循环机制
(1)浏览器环境:在HTML5中定义的规范
  js执行为单线程(不考虑web worker),所有代码皆在执行线程调用栈完成执行;当执行线程任务清空后才会去轮询取任务队列中任务。

  • 任务队列
       浏览器对不同的异步操作,将其添加到任务队列的时机也不同—由浏览器内核的webcore来执行,其包含3种webAPI:
    • DOM Binding:处理DOM绑定事件,若绑定事件触发时,回调函数立即被webcore添加到任务队列中;
    • network:处理ajax请求,在网络请求返回时,才将对应的回调函数添加到队列中;
    • timer:对setTimeout等计时器进行延时处理,当时间到达时才会将回调函数添加到任务队列中;
  • 异步任务类别及执行顺序
    • macrotask(宏任务—task):script中代码、setTimeout、setInterval、I/O、UI render。
    • microtask(微任务): promise、Object.observe、MutationObserver。


      浏览器异步任务执行顺序
      • 具体过程
        (1)执行完主执行线程中的任务(初始执行线程中没有代码,每一个script标签中的代码是一个独立的macrotask)。
        (2)取出Microtask Queue中任务执行直到清空(若microtask一直被添加,则会继续执行microtask,卡死macrotask)。
        (3)取出Macrotask Queue中一个任务执行。
        (4)取出Microtask Queue中任务执行直到清空。
        (5)重复(3)和(4)

(2)node环境:由libuv库实现;
  node基于事件循环实现非阻塞和事件驱动,其事件循环按阶段执行;

Node中的事件循环阶段:每个阶段都有对应的任务队列,一次tick就是完成所有阶段的一次执行

  • 阶段详情
    (1)timers(定时器阶段):处理setTimeout()和setInterval()设定的回调函数队列;

     一个timer事件指定一个下限时间而不是准确的时间,在达到这个下限时间后+主线程空闲时,执行该事件对应的回调函数,从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers(poll阶段会控制是否进入下个timers阶段);
    

    (2)I/O callbacks阶段:执行一些系统操作的回调(比如网络通信的错误回调);

    (3)idle、prepare:仅供libuv内部调用;

    (4)poll(轮询阶段): 等待还未返回的I/O事件,任何异步方法(除timers、setImmediate、close外)完成时,都会将其加到poll queue里,并立即执行;

    • 主要功能:

      (i) 处理poll队列里的事件;
      (ii)执行下限时间已经达到的timers的回调(进入下一个事件循环) ;

    • 当事件循环进入poll阶段:
      (i)poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
      (ii)poll队列为空的时候,这里有两种情况。
      1)如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
      2)如果代码没有被设定setImmediate()设定回调:
      * 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列(进入下一个事件循环阶段了)。
      * 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。

    (5)check阶段:执行setImmediate()设定的回调;
    * setImmediate()实际上是一个特殊的timer,跑在事件循环中的一个独立的阶段;它使用libuv的API来设定在:
    * poll阶段结束后立即执行回调;
    * poll阶段空闲时,不让阻塞在poll阶段直接跳到check阶段执行回调。

    (6)close callbacks阶段:如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

  • 任务队列类型
    原生的libuv事件循环处理的队列有4种主要类型:
    (1)Timers Queue;
    (2)I/O Queue;
    (3)Check Queue;
    (4)Close Queue;
    中间队列有2种:
    (1)Next tick队列:process.nextTick()
    (2)Other Microtasks:包括其他 microtask,如 resolved promise回调;
      ** Next tick队列比Other Microtasks队列具有更高的优先级**;不过,它们都在事件循环的两个阶段之间进行处理,也就是在结束一个阶段后libuv通信回传到上层;

    注:NodeJS中不同类型的事件在自己的队列中排队;中间队列是只要一个阶段完成,事件循环就会检查这两个中间队列是否有可执行的任务,若有则立即处理它们直到为空,一旦为空,事件循环将继续到下一个阶段。

    一次tick的流程

    • 具体过程
      (1)清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
      (2)清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
      (3)清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
      (4)清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
      (5)进入下轮循环(tick);

4.代码

function sleep(time) {
  let startTime = new Date()
  while (new Date() - startTime < time) {}
  console.log('1s over')
}
setTimeout(() => {
  console.log('setTimeout - 1')
  setTimeout(() => {
      console.log('setTimeout - 1 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 1 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 1 - then - then')
      })
  })
  sleep(1000)
})

setTimeout(() => {
  console.log('setTimeout - 2')
  setTimeout(() => {
      console.log('setTimeout - 2 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 2 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 2 - then - then')
      })
  })
  sleep(1000)
})
浏览器输出

node输出

6.Node的异步I/O模型
(1)基本要素:事件循环、观察者、请求对象、IO线程池;

  • 事件循环:典型的生产者/消费者模型;

    • 事件的产生:网络请求、文件IO等操作;
    • 事件的消费:主线程空闲时从观察者那儿取出事件并处理其回调;
  • 观察者:在每个Tick的过程中,通过观察者判断是否有事件需要处理;

      小剧场:
      * 主线程:饭馆的厨房;
      * 观察者:收银台的小妹;
      * 事件及回调函数:客人的点单;
      * 剧情:厨房一轮一轮炒菜,但是具体要炒什么菜取决于收银台收到的客人的下单。
    
  • 请求对象: 从js发起回调到内核执行完IO操作的过渡过程中的中间产物

      以fs.open(path,flags,[mode],callback)打开某个文件为例:从js调用Node的核心模块->核心模块调用C++内建模块->内建模块调用libuv进行系统调用:uv_fs_open():
      (1)创建一个FSReqWrap请求对象:封装js层传入的参数和open()方法,将回调函数设置到该对象的oncomplete_sym属性上;
      (2)将这个请求对象推入线程池中等待执行:当线程池有可用线程时,调用相应的底层函数:fs_open();
       js调用完后立即返回,js线程可以继续执行当前任务的后续操作,当前的I/O操作在线程池中等待操作,不影响js线程。
    
fs.open()流程图
  • 执行回调:以windows平台为例
    • 线程池中的I/O操作调用完后,会将获取的结果存储到req->result属性上,调用PostQueuedCompletionStatus()向IOCP(IO完成端口)提交执行状态,告知当前对象操作已经完成,并将线程归还线程池
    • I/O观察者在每次Tick的执行中,调用GetQueuedCompletionStatus()检查线程池是否有执行完的请求,若存在,将请求对象加入到I/O观察者队列中,然后将其当作事件处理:取出请求对象的result属性做参数,取出oncomplete_sym属性做方法,然后调用执行。

(2)基本流程

  • 第一阶段:组装好对象 -> 送入IO线程池等待执行;
  • 第二阶段:回调通知;


    node中整个异步IO的流程

[参考文献]
1.NodeJS事件循环(英文版/中文版)
2.node中的Event模块
3.浏览器和Node不同的事件循环(Event Loop)
4.《深入浅出nodejs》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容