对于 JS 单线程语言进行异步操作需要一个事件循环机制来保证任务的执行,而不是遇到一步任务就会阻塞主线程,Event Loop 应运而生
进程与线程

- 进程好比图中的工厂,有单独的专属自己的工厂资源。
- 线程好比图中的工人,多个工人在一个工厂中协作工作,工厂与工人是 1:n的关系。也就是说一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 工厂的空间是工人们共享的,这象征一个进程的内存空间是共享的,每个线程都可用这些共享内存。
- 多个工厂之间独立存在。
以Chrome浏览器中为例,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程(下文会详细介绍),比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。
一个浏览器进程包含的线程
当前执行栈为空的时候任务会从微任务中出队,压入到栈中,如果微任务队列为空,就从宏任务队列中出队。
Node 中的 Event Loop
推荐教程:https://www.bilibili.com/video/BV13A4y1Q7N5?spm_id_from=333.337.search-card.all.click&vd_source=76172f134c7a6e677d8368b9769b2713
如果用 Java 去读一个文件,这是一个阻塞的操作,在等待数据返回的过程中什么也干不了,因此就开一个新的线程来处理文件读取,读取操作结束后再去通知主线程。
这样虽然行得通,但是代码写起来比较麻烦。像 Node.js V8 这种无法开一个线程的怎么办?
循环阶段(网上教程看起来真复杂)
在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:
- timers:执行满足条件的setTimeout、setInterval回调。
- I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
- idle,prepare:可忽略
- poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
- check:执行setImmediate的回调。
- close callbacks:关闭所有的closing handles,一些onclose事件。
Node 中异步操作的分类
事件循环

当执行栈为空的时候,会去Timer 队列中看是否有任务可以执行,如果没有就进入到 Poll 队列,Poll 队列如果也为空,此刻会判断 Timer 队列 和 Check 队列是否都为空,如果都为空的话,那么不再继续循环,而是等待有新的任务加入到这三个队列中来。
从设计上,事件循环优先处理 IO 事件,因为当事件循环队列为空的时候,指针指向的是 Poll 队列 !
不同的输出结果:
⚠️: setImmediate 会直接将任务推入到 Check 队列
多次允许后发现,执行结果可能为以下两种情况setTimeOut(()=>{console.log('1')},0)setImmediate(()=>{console.log('2')})
- 1 2
- 2 1
分析:在 Node 中定时器的最小单位是 1ms,如果计时器执行的够快,在事件循环还未启动的时候就将任务推入到 Timer 队列中那么是 1 先打印,相反如果执行的稍微慢一点,会先进入到 Check 队列,那么先打印 2
当事件循环已经启动的时候,setImmediate 执行顺序总是早于定时器任务。
Process.nextTick
nextTick 任务会被插入到异步模块中的nextTick 队列,每一个事件循环叫做一个 Tick,nextTick 就是插入到 Tick 的头部,所以 nextTick 队列中的任务总是优先于事件循环
微任务队列
微任务队列和 nextTick 类似,执行顺序在 nextTick 之后,在 Tick 事件循环之前。
总结
在 node 环境中,JS 总是自上而下进行执行的,同步代码阻塞执行,异步代码加入到异步模块,以非阻塞的方式进行执行,对于的异步回调函数会在异步任务执行完毕之后被派发到不同的队列当中。
同步代码执行完毕之后先执行 nextTick 队列里面的任务,在执行微任务队列里面的任务,最后执行 Tick 事件循环,Tick 事件循环包含三个队列,分别是:Timer 、 Poll 和 Check 队列。执行顺序也是从左到右,但是当执行到 Poll 队列之后,Node 会判断 这三个队列中是否还有未执行完毕的任务,如果没有的话就停在 Poll 队列,等待新的任务加入到 TIck之中。

