事件循环和异步IO

事件循环

___事件循环:编写的JavaScript代码浏览器或者Node之间的桥梁

进程和线程

进程

计算机当中已经运行的程序

  • 在计算机里启动一个应用程序默认就会由操作系统开启新的进程(可能是多个进程)
  • 可以说进程是线程的容器

线程

操作系统能够运行运算调度的最小单元

  • 每一个进程中,都会启动一个线程用来执行程序中的代码,这个线程被称之为主线程

多进程多线程开发

操作系统是如何做到同时让多个进程(边听歌、边写代码、边查阅资料)同时工作呢?

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换;
  • 当我们的进程中的线程获取获取到时间片时,就可以快速执行我们编写的代码;
  • 对于用户来说是感受不到这种快速的切换的;

浏览器 JS

JavaScript的进程有自己的容器进程:浏览器或者Node

浏览器是多进程的 每个进程中又有很多线程 包括JavaScript代码的线程

JavaScript的代码执行是在一个单独的线程中执行的:

  • 意味着JavaScript的代码,在同一个时刻只能做一件事
  • 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞

JavaScript执行过程

函数调用

函数被调用 压入调用栈 执行完 出栈

当在执行JavaScript代码的过程中,有异步操作时,并不会阻塞后续代码的执行;

  • 比如 settimeout是调用了webapi 在合适的时机,会将timer函数加入到一个事件队列中
  • 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行

浏览器事件循环

浏览器将异步任务回调函数进行一个保存(保存到了一个红黑树里面) 之后当这些回调函数到了触发的条件的时候 把回调函数放到事件队列中 然后按照队列先进先出的规则在主线程函数调用栈中执行函数完成任务()

宏任务和微任务

___事件循环中并非只维护着一个队列,事实上是有两个队列:

  • 宏任务(macrotask): 如DOM监听 , XMLHttpRequest , 定时器,UI Rendering 等
    • 宏任务会被加入宏任务队列
  • 微任务(microtask): Promise.then((data)=>{}) , Mutation Observer API , queueMicrotask(自定义微任务)
    • 微任务会被加入微任务队列
  • 浏览器优先执行微任务队列 , 并且是将微任务队列里的任务全部执行完成 再执行宏任务队列
    • 在执行任何一个宏任务之前(不是队列 是任务),都会先查看微任务队列中是否有任务需要执行
      • 也就是宏任务执行之前 必须保证微任务队列是空的
      • 如果不为空 那么就优先执行微任务队列中的任务(回调)
  • async 和 await是promise 的语法糖
    • await后面跟着的那句代码相当于resolve()那里的代码 再下面的代码相当于then里面的代码

Node中的事件循环

Node架构分析

___浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

  • libuv中主要维护了一个EventLoop和worker threads(线程池);
  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
  • libuv是一个多平台的专注于异步IO的库

阻塞IO和非阻塞IO

如果我们希望在程序中对一个文件进行操作,那么我们就需要通过文件描述符打开这个文件

我们任何程序中的文件操作都是需要进行系统调用,事实上对文件的操作,是一个操作系统的IO操作(输入、输出);

操作系统为我们提供了阻塞式调用非阻塞式调用

  • 阻塞式调用
    • 调用结果返回之前,当前线程处于阻塞态(当阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行
  • 非阻塞式调用
    • 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可
    • 在开发中很多耗时的操作可以基于这样的非阻塞式调用
      • 比如网络请求使用了Socket 通信 ,而Socket本身提供了Select模型 可以进行非阻塞方式的工作
      • 还有文件读写的IO操作 可以使用操作系统提供的基于事件的回调机制;
    • 存在的问题
      • 我们并没有获取到需要读取(我们以读取为例)的结果
      • 所以我们想要知道是否读取到了完成的数据 就要频繁的确定读取的数据是否是完整的
        • 这个读取过程称之为轮训操作
      • libuv提供了一个线程池(Thread Pool):
        • 线程池会负责轮训所有相关的操作,并且会通过轮询等方式等待结果
        • 当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中;
        • 事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数;

阻塞和非阻塞,同步和异步

阻塞和非阻塞是对于被调用者来说的

  • 在我们这里就是系统调用,操作系统为我们提供了阻塞调用和非阻塞调用

同步和异步是对于调用者来说的

  • 在我们这里就是自己的程序;
  • 如果我们在发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用;
  • 如果我们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用;

___ Libuv采用的就是非阻塞异步IO的调用方式

Node 的事件循环 的阶段

___事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道

  • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中
  • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行

一次完整的事件循环Tick的阶段:

  • 定时器:本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)

Node 的宏任务和微任务

宏任务 (macrotask): setTimeout setInternal IO事件 setImmediate close事件

微任务 (microtask):Promise的then回调、process.nextTick、queueMicrotask;

面试题环节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}

async function async2() {
console.log('async2')
}

console.log('script start')

setTimeout(function () {
console.log('setTimeout0')
}, 0)

setTimeout(function () {
console.log('setTimeout2')
}, 300)


setImmediate(() => console.log('setImmediate'));

process.nextTick(() => console.log('nextTick1'));

async1();

process.nextTick(() => console.log('nextTick2'));

new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})

console.log('script end')

// script start
// async1 start
// async2
// promise1
// promise2
// script end
// nextTick1
// nextTick2
// async1 end
// promise3
// setTimeout0
// setImmediate
// setTimeout2
1
2
3
4
5
6
7
8
9
10
11
setTimeout(() => {
console.log("setTimeout");
}, 0);

setImmediate(() => {
console.log("setImmediate");
});

// 问题: setTimeout setImmediate 有两种顺序
setTimeout setImmediate
setImmediate setTimeout
  • 出现这种情况的原因

    在Node源码的deps/uv/src/timer.c中141行,有一个 uv__next_timeout的函数;这个函数决定了,poll阶段要不要阻塞在这里;阻塞在这里的目的是当有异步IO被处理时,尽可能快的让代码被执行; 他控制的要不要执行后续

    官方文档中:轮询 阶段 控制何时定时器执行。

    • 如果事件循环开启的时间(ms)是小于 setTimeout函数的执行时间的;
      • 也就意味着先开启了event-loop,但是这个时候执行到timer阶段,并没有定时器的回调被放到入 timer queue中;
      • 所以没有被执行,后续开启定时器和检测到有setImmediate时,就会跳过poll阶段,向后继续执行;
      • 这个时候是先检测 setImmediate,第二次的tick中执行了timer中的setTimeout;
    • 如果事件循环开启的时间(ms)是大于 setTimeout函数的执行时间的;
      • 这就意味着在第一次 tick中,已经准备好了timer queue;
      • 所以会直接按照顺序执行即可;
  • 另外

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    setTimeout(() => {
    setTimeout(() => {
    console.log('setTimeout');
    }, 0);
    setImmediate(() => {
    console.log('setImmediate');
    });
    }, 0);

    //结果是setImmediate setTimeout
  • 原因是如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关

    ____附上个人认为写的比较好的一篇博客

https://set.sh/post/200317-how-nodejs-event-loop-works#%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%E6%A6%82%E8%BF%B0

以及官方文档中的对Node 事件循环的解释:

https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/#poll