进程,线程,浏览器的事件循环,Node的事件循环,宏任务,微任务,执行顺序判断面试题
1. 进程和线程
1.1. 进程和线程概念
线程和进程是操作系统中的两个概念
- 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式
- 线程(thread):操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中
简单理解
进程:启动一个应用程序,就会默认启动一个进程(也可能是多个进程)
线程:每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程
所以也可以说进程是线程的容器
再用一个形象的例子解释
- 操作系统类似于一个大工厂
- 工厂中里有很多车间,这个车间就是进程
- 每个车间可能有一个以上的工人在工厂,这个工人就是线程
1.2. 操作系统 – 进程 – 线程

1.3. 操作系统的工作方式
操作系统是如何做到同时让多个进程(边聊天、边写代码、边查阅资料)同时工作呢?
- 这是因为CPU的运算速度非常快,它可以快速的在多个进程之间迅速的切换
- 当进程中的线程获取到时间片时,就可以快速执行我们编写的代码
- 对于用户来说是感受不到这种快速的切换的
可以在Mac的活动监视器或者Windows的资源管理器中查看到很多进程

1.4. 浏览器中的JavaScript线程
经常会说
JavaScript是单线程,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node浏览器是一个进程吗,它里面只有一个线程吗?
- 目前多数的浏览器其实都是多进程的,打开一个tab页面时就会开启一个新的进程,这是为了防止一个页面卡死而造成所有页面无法响应,整个浏览器需要强制退出
- 每个进程中又有很多的线程,其中包括执行JavaScript代码的线程
JavaScript的代码执行是在一个单独的线程中执行的
- 这就意味着JavaScript的代码,在同一个时刻只能做一件事
- 如果这件事是非常耗时的,就意味着当前的线程就会被阻塞
所以真正耗时的操作,实际上并不是由JavaScript线程在执行的
- 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作
- 比如网络请求、定时器,我们只需要在特性的时候执行应该有的回调即可
2. 浏览器的事件循环
- 如果在执行JavaScript代码的过程中,有异步操作呢?
- 插入了一个setTimeout的函数调用
- 这个函数被放到入调用栈中,执行会立即结束,并不会阻塞后续代码的执行
1 | function sum(num1,num2){ |

2.1. 宏任务和微任务
事件循环中并非只维护着一个队列,事实上是有两个队列
- 宏任务队列(macrotask queue):ajax、setTimeout、setInterval、DOM监听、UI Rendering等
- 微任务队列(microtask queue):Promise的then回调、 Mutation Observer API、queueMicrotask()等
那么事件循环对于两个队列的优先级是怎么样的呢?
- main script中的代码优先执行(编写的顶层script代码)
- 在执行任何一个宏任务之前(不是队列,是一个宏任务),都会
先查看微任务队列中是否有任务需要执行- 也就是宏任务执行之前,必须保证微任务队列是空的
- 如果不为空,那么就优先执行微任务队列中的任务(回调)
2.2. 面试题
2.2.1. 面试题一
1 | setTimeout(function () { |

2.2.2. async/await
await之后的语句相当于放在promise的then中
1 | async function bar() { |

2.2.3. 面试题二
1 | async function async1() { |

2.2.4. 面试题三
- promise的then函数中直接return值,相当于 resolve(值)
- promise的then函数中return的是实现了thenable的对象,则接下来的then会被安排到下一次微任务中,以防止thenable中有大量计算
- promise的then函数中return的是promise对象,则会因为非普通值而被安排到下一次微任务,又因为是Promise.resolve则then又被安排到下一次微任务,错过两次微任务
1 | Promise.resolve().then(() => { |

3. Node的事件循环
浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由 libuv实现的
Node的架构图
- libuv中主要维护了一个EventLoop和worker threads(线程池)
- EventLoop负责调用系统的一些其他操作:文件的IO、Network、child-processes等
libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方

3.1. Node事件循环的阶段
事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道
- 无论是文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函 数放到事件循环(任务队列)中
- 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行
但是一次完整的事件循环Tick分成很多个阶段
定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数待定回调(Pending Callback):对某些系统操作(如TCP错误类型)执行回调,比如TCP连接时接收到ECONNREFUSEDidle, prepare:仅系统内部使用轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调检测(check):setImmediate() 回调函数在这里执行关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)
3.2. Node事件循环的阶段图解

3.3. Node的宏任务和微任务
从一次事件循环的Tick来说,Node的事件循环更复杂,也分为微任务和宏任务
- 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件
- 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask
但是,Node中的事件循环不只是微任务队列和 宏任务队列
- 微任务队列
- next tick queue:process.nextTick
- other queue:Promise的then回调、queueMicrotask
- 宏任务队列
- timer queue:setTimeout、setInterval
- poll queue:IO事件
- check queue:setImmediate
- close queue:close事件
- 微任务队列
3.4. Node事件循环的顺序
- 在每一次事件循环的tick中,会按照如下顺序来执行代码
- next tick microtask queue
- other microtask queue
- timer queue
- poll queue
- check queue
- close queue
3.5. Node执行面试题
1 | async function async1() { |
