1. 背景
1.1 单线程
我们都知道 JavaScript 是单线程的, 这是它语音特性决定的, 因为的主要用途 在于一些 I/O 操作, Dom 操作等, 单线程为它提高了效率, 但同时有很多操作, 比如读取文件, Ajax 获取数据等, 都是一些比较耗时的操作, 用户等不了太长时间, 因此衍生出了事件循环这个概念.
1.2 任务队列
在 JavaScript 中, 我们把队列分为两种, 一种是同步任务, 在主线程是执行, 只有一个任务执行完才可以执行下一个任务, 一种是异步任务, 它不进入主线程, 而是通过另外一种方式, 叫做任务队列
例如上图中
所有同步任务在 Stack(栈) 中执行
其他耗时操作在下面的 Queue(队列) 中执行, 一旦这些耗时操作执行完, 就会放入队列中, 先进先出
一旦执行栈中任务执行结束, 系统开始读取在队列中排队的任务, 每访问一个队列, 会执行完全部相同代码, 比如 setTimeout 定时队列, 再去下一个队列
整个过程不停循环, 称为事件循环
2 Node.js
2.1 事件循环
Node.js 中也有 Event Loop, 不过它的机制和和浏览器中的略微不同
如上图是经典的 Node.js 中的事件循环图, Node.js 采用谷歌 V8 作为解析引擎, 在 I/O 处理方面使用了 libuv 库, 它是一个基于事件驱动的跨平台抽象层, 封装了不同操作系统一些底层特性, 对外提供统一的 API, 上图的事件循环机制是它里面的实现. 每次事件循环都包含了六个阶段:
timers 阶段: 包括 setTimeout,setInterval 等
pedding callbacks: 执行一些系统调用错误
idle,prepare: node 内部使用
poll: 轮询阶段, 检查 I/O 队列中定时器是否到时, 例如 (读取文件)
check: 执行 setImmediate() 设定的 callbacks
close callbacks: 关闭回调
其中还有一些微任务和宏任务的概念 微任务: promise.then process.nextTick(), 其中 nextTick() 执行会比前者快 宏任务: setTimeout setImmidate(ie) messageChannel 等
微任务总是会比宏任务先执行
2.2 例子
- setTimeout(() => {
- console.log('timeout1');
- process.nextTick(()=>{
- console.log('nextTick1');
- })
- }, 1000);
- setTimeout(()=>{
- console.log('timeout2')
- },1000)
复制代码
结果: timeout1 => timeout2 =>nextTick1 原因: 首先都是外面宏任务, 先执行时间队列, 从上到下依次执行完 timeout1=>timeout2, 执行完后, 到下一个队列, 执行 nextTick()
- let fs = require('fs');
- fs.readFile('./index.html',function(){
- setImmediate(function(){
- console.log('setImmediate')
- });
- setTimeout(function(){
- console.log('setTimeout')
- },0); // ->4
- })
复制代码
结果: setImmediate => setTimeout 原因: 读取文件是 I/O 操作是在 poll 轮询阶段, 读取完后下一阶段是 check, 看有没有 setImmediate, 所以先执行这个, 再执行 setTimeout
- let fs = require('fs');
- setTimeout(function(){
- Promise.resolve().then(()=>{
- console.log('then2');
- })
- },0);
- Promise.resolve().then(()=>{
- console.log('then1');
- });
- fs.readFile('./index.html',function(){
- process.nextTick(function(){
- console.log('nextTick')
- })
- setImmediate(()=>{
- console.log('setImmediate')
- });
- });
复制代码
结果: then1 => then2=> nextTick => setImmediate 原因: 首先看最外面有微任务 Promise.then, 所以 then1, 接着到时间队列, times, 执行 then2,times => poll 是不同阶段, 每执行不同阶段, process.nextTick 不属于事件循环的任何一个阶段, 它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调. 所以 都要检查当前有没有 promise, 或者 nextTick, 如果有, 先执行这些, 有 nextTick, 再执行 setImmediate
来源: https://juejin.im/post/5b600192e51d451958671aef