Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event Loop原理 #21

Open
Genluo opened this issue Aug 31, 2019 · 0 comments
Open

Event Loop原理 #21

Genluo opened this issue Aug 31, 2019 · 0 comments

Comments

@Genluo
Copy link
Owner

Genluo commented Aug 31, 2019

Event Loop 是计算机中的一种运行机制,Javascript采用这种结构来解决单线程带来的一些问题,目前主要分为两种,一种是浏览器端实现的事件循环,另一种就是Node端实现的事件循环,两者有点不同,所以在执行某些代码的时候我们会发现可能同样的代码在两种不同环境下输出的结果是不同,那么为了理解这种不同的差异,我们有必要深入了解下浏览器和Node中不同环境下实现的Event loop

浏览器中实现的Event Loop

因为JavaScript是一种单线程语言,因此为了实现主线程不阻塞,Event Loop这样的方案应运而生,

在浏览器中主要将任务分为两种,一种是macro task(微任务),一种是micro task(宏任务)

  • macro task主要包含:setTimeout、setInterval、setImmediate、I/O、UI交互事件

  • micro task主要包含:Promise、process.nextTick、MutaionObserver

根据规范来讲,浏览器主要是这样处理的:

  • 检查最旧的task(一次),可以将script中的代码视作一个初始的 Task
  • 检查是否存在micro task,然后不停执行,直到micro task清空
  • 执行render(页面渲染)
console.log(1)

setTimeout(() => {
    console.log(2)
    new Promise(resolve => {
        console.log(4)
        resolve()
    }).then(() => {
        console.log(5)
    })
})

new Promise(resolve => {
    console.log(7)
    resolve()
}).then(() => {
    console.log(8)
})

setTimeout(() => {
    console.log(9)
    new Promise(resolve => {
        console.log(11)
        resolve()
    }).then(() => {
        console.log(12)
    })
})

这个函数的执行结果为1、7、8、2、4、5、9、11、12

img

Node中实现的EventLoop

Node中的Event loop底层是libuv的event loop,Node也是将任务分为宏任务和微任务两种类型,整个事件循环过程如下:

   ┌───────────────────────┐
┌─>│        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', ...)
   └───────────────────────┘
  • 定时器: 本阶段执行已经安排的setTimeoutsetInterval的回调函数
  • 待定回调:执行延迟到下一个循环迭代的I/O回调
  • idle、prepare:仅系统内部使用
  • 轮询:检索新的I/O事件,执行与I/O相关的回调(几乎所有情况下,除了关闭的回调函数,它们由计时器和setImmediate排定的之外),其余情况node将在此处阻塞
  • 检测:setImmediate回调函数将在这里执行
  • 关闭的回调函数:一些准备关闭的回调函数,比如: socket.on('colse',...)

Node的事件循环和浏览器本质不同

Node 中宏任务分为了几种,并且每一种都存在自己的task queue, 同时我们可以将Process.nextTick看做是一种微任务,优于其他所有的微任务而执行,并且所有 MicroTask(微任务) 的执行时机,是不同类型 MacroTask(宏任务) 切换的时候,执行过程可以参考下面这张图:

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

img

细节问题

细节一:setTimeout 与 setImmediate 的顺序
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')
  })
})

这个函数可以保证函数执行在Poll阶段,然后是check阶段,所以首次应该是setImmediate执行,本质是我们执行里面回调函数是在poll阶段,这个阶段的下一个阶段直接就是check,所以后面首先会进入check阶段执行setImmediate的任务

细节二: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 阶段。

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

细节四: IO饥饿问题

根据上文中整个事件流程,我们可以知道,process.nextTick()会在各个事件阶段之间执行,一旦执行,要等到nextTick队列被清空,才会进入到下一个事件阶段,所以如果递归调用process.nextTick,这样的话就不能保证Node进入下一个Tick,这样的话就有可能带着IO的callback函数不能是实现,出现IO饥饿问题。

细节五:如何合理使用setImmediate和setTimeout

这个要从从自己的实际开发和项目要求来说,入股为了避免阻塞IO,应该使用setImmediate,因为他的执行在poll阶段之后执行。

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant