什么是事件循环?

事件循环是什么?

  • 事件循环可以理解成我们编写的JavaScript和浏览器或者Node之间的一个桥梁。

浏览器的事件循环是一个我们编写的JavaScript代码和浏览器API调用(setTimeout/AJAX/监听事件等)的一个桥梁,桥梁之间它们通过回调函数进行沟通。

Node的事件循环是一个我们编写的JavaScript代码和系统调用(file system、network等)之间的一个桥梁,桥梁之间它们通过回调函数进行沟通。
image.png

进程和线程

线程和进程是操作系统中的两个概念:

  • 进程(process):计算机已经运行的程序
  • 线程(thread):操作系统能够运行运算调度的最小单位

听起来很抽象,更直观的解释:

  • 进程:我们可以认为,启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
  • 线程:每一个进程中,都会启动一个线程用来执行程序中的代码,这个线程被称之为主线程
  • 所以我们也可以说进程是线程的容器

再举一个形象的例子解释:

  • 操作系统类似于一个工厂
  • 工厂里有很多车间,这个车间就是进程
  • 每个车间至少有一个或以上的工人在工作,这个工人就是线程

    多进程多线程开发

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

  • 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换

  • 当我们的进程中的线程获取到时间片时,就可以快速执行我们编写的代码
  • 对于用户来说是感受不到这种快速的切换的

image.png

浏览器和JavaScript

我们经常会说JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node

浏览器是一个进程吗?它里面只有一个线程吗?

  • 目前多数的浏览器其实都是多进程的,当我们打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡斯而造成所有页面无法响应,整个浏览器需要强制退出。
  • 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程

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

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

    JavaScript执行过程

    ```javascript const message = “hello world”;

console.log(message);

function sum(num1, num2) { return num1 + num2; }

function foo() { const result = sum(20, 30); console.log(result); }

foo();

  1. 上面代码的执行过程:
  2. - 定义常量message
  3. - 执行log函数,函数会被放入到调用栈中执行,输出message的值,执行完毕后出栈
  4. - 执行foo函数,函数被被放入到调用栈中执行,但是不是马上执行完并出栈
  5. - foo中调用了sum函数,所以sum函数被放入到调用栈中执行
  6. - sum函数执行完毕后出栈,并将结果给常量result
  7. - 执行log函数,函数会被放入到调用栈中执行,输出result的值,执行完毕后出栈
  8. - foo函数完全执行完毕,出栈
  9. 如果在执行的过程中,有异步操作呢?
  10. - 我们在代码中插入一个setTimeout的函数调用
  11. - 这个函数被到调用栈中,执行会立即结束,并不会阻塞后续代码的执行
  12. ```javascript
  13. setTimeout(() => {
  14. console.log("setTimeout");
  15. }, 1000);

那么,传入的一个函数(比如我们称之为timer函数),会在什么时候被执行呢?

  • 事实上,setTimeout时调用了web api,在合适的时机,会将timer函数加入到一个事件队列中
  • 事件队列中的函数,会被放入到调用栈中,在调用栈中被执行

image.png

宏任务和微任务

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

  • 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
  • 微任务队列(microtask queue):Promise的then回调、Mutation Observer API、queueMicrotask()等

那么事件循环对于两个队列的优先级是怎么样的呢?

  1. main script中的代码优先执行(编写的顶层script代码)
  2. 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会先查看为任务队列中是否有任务需要执行
    1. 也就是宏任务执行之前,必须保证微任务队列是空的
    2. 如果不为空,那么就优先执行为任务队列中的任务(回调)

async、await是Promise的一个语法糖:

  • 我们可以将await关键字后面执行的代码,看作是包裹在(resolve,reject)=> { 函数执行 } 中的代码
  • await的下一条语句,可以看做是 then( res => { 函数执行 } ) 中的代码

    面试题一:

    ```javascript setTimeout(function () { console.log(“set1”);

    new Promise(function (resolve) { resolve(); }).then(function () { new Promise(function (resolve) {

    1. resolve();

    }).then(function () {

    1. console.log("then4");

    }); console.log(“then2”); }); });

new Promise(function (resolve) { console.log(“pr1”); resolve(); }).then(function () { console.log(“then1”); });

setTimeout(function () { console.log(“set2”); });

console.log(2);

queueMicrotask(() => { console.log(“queueMicrotask”); });

new Promise(function (resolve) { resolve(); }).then(function () { console.log(“then3”); });

  1. <a name="Ay3EE"></a>
  2. ## 面试题二:
  3. ```javascript
  4. async function async1() {
  5. console.log("async1 start");
  6. await async2();
  7. console.log("async1 end");
  8. console.log(1);
  9. }
  10. async function async2() {
  11. console.log("async2");
  12. }
  13. console.log("script start");
  14. setTimeout(function () {
  15. console.log("setTimeout");
  16. }, 0);
  17. async1();
  18. new Promise(function (resolve) {
  19. console.log("promise1");
  20. resolve();
  21. }).then(function () {
  22. console.log("promise2");
  23. });
  24. console.log("script end");

Node的架构分析

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

libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方
image.png
Node架构图介绍:

  • 我们会发现libuv中主要维护了一个EventLoop和worker threads(线程池)
  • EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等

阻塞IO和非阻塞IO

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

  • 思考一个问题:JavaScript能直接对一个文件进行操作吗?
  • 看起来是可以的,但是事实上我们任何程序中的文件操作都是需要进行系统调用的(操作系统的文件系统)。
  • 事实上对文件的操作,是一个操作系统的系统调用(IO系统,I是Input输入,O是Output输出)

操作系统通常为我们提通了两种调用方式:阻塞式调用和非阻塞式调用:

  • 阻塞式调用:调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行
  • 非阻塞式调用:调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

所以我们开发中的很多耗时操作,都可以基于这样的非阻塞式调用:

  • 比如网络请求本身使用了Socket通信,而Socket本身提供了select模型,可以进行非阻塞方式的工作
  • 比如文件读写的IO操作,我们可以使用操作系统提供的基于事件的回调机制

    非阻塞IO的问题

    但是非阻塞IO也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果

  • 那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的

  • 这个过程我们称之为轮询操作

那么这个轮询的工作由谁来完成呢?

  • 如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能
  • 并且开发中我们可能不只一个文件的读写,可能是多个文件
  • 而且可能是多个功能:网络的IO、数据库的IO、子进程调用

libuv提供了一个线程池(Thread Pool)

  • 线程池会负责所有相关的操作,并且会通过轮询或者其他的方式等待结果
  • 当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中
  • 事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数

    阻塞和非阻塞,同步和异步的区别

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

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

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

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

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

Node事件循环的阶段

我们前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:

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

但是一次完整的事件循环Tick分成很多个阶段:

  • 定时器(Timers):本阶段执行已经被setTimeout()和setInterval()的调度回调函数
  • 待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSED
  • idle,prepare:仅系统内部使用
  • 轮询(Poll):检索新的I/O事件,执行与I/O相关的回调
  • 检测:setImmediate()回调函数在这里执行
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’,…)

我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

  • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件
    • 优先级: setTimeout / setInterval > I/O callbacks > setImmediate
  • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask
    • process.nextTick > Promise的then回调 > MutationObserver

      面试题一:

      ```javascript async function async1() { console.log(“async1 start”); await async2(); console.log(“async 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”);

  1. <a name="HlX77"></a>
  2. ## 面试题二:
  3. ```javascript
  4. setTimeout(() => {
  5. console.log("setTimeout");
  6. }, 0);
  7. setImmediate(() => {
  8. console.log("setImmediate");
  9. });

问题:以上代码的打印顺序以及为什么。