node事件循环中事件执行顺序

简介: 本文详细介绍了Node.js环境下的事件循环机制,包括其六个主要阶段:timers、I/O callbacks、idle, prepare、poll、check和close callbacks。文章通过具体代码示例解释了`setTimeout`、`setImmediate`和`process.nextTick`在事件循环中的执行顺序和区别。还探讨了在不同情况下(如I/O操作中)这些函数的执行顺序如何受到影响。最后,通过一个综合例子,展示了实际编码中事件循环的执行顺序。

事件循环

在浏览器环境下我们的js有一套自己的事件循环,同样在node环境下也有一套类似的事件循环。



浏览器环境事件循环

首先,我们先来回顾一下在浏览器的事件循环:

总结来说:

首先会运行主线程的同步代码,每一行同步代码都会被压入执行栈,每一行异步代码会压入异步API中(如:定时器线程、ajax线程等;),在执行栈没有要执行的代码时,也就是我们当前主线程没有同步代码了,任务队列会从我们的异步任务微任务队列中取一个微任务放到我们的任务队列中进行执行,将它的回调函数进而再次放到执行栈中进行执行,当微任务队列为空时,会在宏任务中取异步任务加到任务队列,进而压入执行栈,执行回调函数,然后继续在该宏任务中查找同步、异步任务,一次循环,完成了一个事件循环(事件轮询)

浏览器环境下的例子:

例子:

        console.log("1");
        setTimeout(() => {
   
            console.log("setTimeout");
        }, 1);
        new Promise((res, rej) => {
   
            console.log("Promise");
            res('PromiseRes')
        }).then(val => {
   
            console.log(val);
        })
        console.log("2");

分析:
首先执行栈找到第一行的同步代码,直接扔到执行栈中执行,打印1,随后为定时器setTimeout,为异步任务,将代码放到异步对列中等待执行,随后执行promise中的代码,我们要清楚promise是同步执行,它的回调是异步执行,所有打印Promise,将res(‘PromiseRes’)放到异步对列中等待执行,这个时候又遇到了同步代码,打印2,当前主线程的同步代码全部执行完毕,并且执行栈中没有要执行的同步代码,这个时候webApi会从异步队列中去微任务队列中的第一个,加入到事件队列执行,将返回的回调函数压入到执行栈中执行,打印PromiseRes,随后微任务执行完毕,已经没有微任务,现在就需要从宏任务队列中取宏任务定时器,加入到任务队列中,将回调函数压入到执行栈中执行,打印setTimeout。

node环境事件循环

在node中事件循环主要分为六个阶段来实现:

外部数据输入–》轮询阶段–》检查阶段–》关闭事件回调阶段–》定时器阶段–》I/O回调阶段–》闲置阶段–》轮询阶段》…开始循环

六个阶段

图片来自网络
在这里插入图片描述
timers阶段:用来执行timer(setTimeout,setInterval)的回调;
I/O callbacks阶段:处理一些上一轮循环中少数未执行的I/O回调
idle,prepare 阶段:仅node内部使用,我们用不到;
poll阶段:获取新的I/O时间,适当的条件下node将阻塞在这里;
check阶段:执行setImmediate()的回调;
close callbacks 阶段:执行socket的close时间回调

主要阶段
timer:
timers阶段会执行setTimeout和setInterval回调,并且是由poll阶段控制的。
同样,在node中定时器指定的时间也不是准确时间,只能是尽快执行。
poll:
poll这一阶段中,系统会做两件事情:
1.回到timer阶段执行回调
2.执行I/O回调
并且在进入该阶段时如果没有设定了timer 的话,会发生以下两件事情

如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果 poll 队列为空时,会有两件事发生
1、如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
2、如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

check阶段
setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后,在进入check阶段执勤poll会检查有的话到check阶段,没有的换直接到timer阶段。

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

setImmediate 设计在 poll 阶段完成时执行,即 check 阶段,只有在check阶段才会执行;
setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行,表示当前线程没有其他可执行的同步任务,才会在timer阶段执行定时器。

这两个执行的时机可前可后:
例子1:

// //异步任务中的宏任务
setTimeout(() => {
   
    console.log('===setTimeout===');
},0);
setImmediate(() => {
   
    console.log('===setImmediate===')
})

在这里插入图片描述

多次重复执行的结果会不同,有一种随机的感觉,出现这种情况的原因主要和setTimeout的实现代码有关,当我们不传时间参数或者设置为0的时候,nodejs会取值为1,即1ms(在浏览器端可能取值会更大一下,不同浏览器也各不相同),所以在电脑cpu性能够强,能够在1ms内执行到timers phase的情况下,由于时间延迟不满足回调不会被执行,于是只能等到第二轮再执行,这样setInterval就会先执行。
可能由于cpu多次执行相同任务用时会有细微差别,而且在1ms上下浮动,才会造成上面的随机现象
一般情况下setTimeout为0时候会在setImmediate之前执行

例子2:
当我们传入的值大于定时器timer执行的回调时间的时候会直接导致定时器在下一次事件循环中执行

setTimeout(() => {
   
    console.log('===setTimeout===');
},10);
setImmediate(() => {
   
    console.log('===setImmediate===')
})

在这里插入图片描述
例子3:
当我们将上述代码放入一个i/o中就会固定先check再而timer:


const fs = require('fs');

fs.readFile("./any.js", (data) => {
   
    setTimeout(() => {
   
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
   
        console.log('===setImmediate===')
    })
});

在这里插入图片描述
在第一轮循环中读取文件,在回调中,会进入check阶段进而执行setImmediate,随后timer阶段执行定时器。
setimmediate 与 settimeout 放入一个 I/O 循环内调用,则 setImmediate 总是被优先调用

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

例子1:

setTimeout(() => {
   
 console.log('timer1')
 Promise.resolve().then(function() {
   
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
   
 console.log('nextTick')
 process.nextTick(() => {
   
   console.log('nextTick')
   process.nextTick(() => {
   
     console.log('nextTick')
     process.nextTick(() => {
   
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

例子2:


const fs = require('fs');

fs.readFile("./any.js", (data) => {
   
    process.nextTick(()=>console.log('process===2'))
    setTimeout(() => {
   
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
   
        console.log('===setImmediate===')
    })
});
process.nextTick(()=>console.log('process===1'))

在这里插入图片描述

练习例子

async function async1() {
   
    console.log('2')
    //会等待await执行完 但是不会向下执行 因为下面输入微任务
    await async2()
    console.log('9')
  }

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

  console.log('1')

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

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

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

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

  async1();

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

  new Promise(function (resolve) {
   
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
   
    console.log('10')
  })

  console.log('6')

分析:
上面的循序就是序号的顺序;
首先打印1:
前面都是两个函数声明,所有直接打印1,这行同步代码;
打印2:
打印完1后,都是异步代码,加入异步任务队列,直接到async1函数调用,在这个函数中打印2;
打印3:
async1这个函数是个async await函数,所有也是一个变相的同步操纵等待async2函数执行,async2执行后并不会直接打印9,原因await接受的是一个promise的then操作,所以后面属于一个promise的回调操作属于微任务,加入微任务队列;
打印4:
process.nextTick为微任务,所以会继续执行promise,打印4;
打印5:
resolve()的回调不会立即执行属于微任务,加入微任务队列,所以打印5;
打印6:
最后一个主线程的同步代码,打印6;
打印7、8:
process.nextTick优先级高于其他定时器,所以会直接执行回调函数打印7、8;
打印9、10:
这个时候需要执行微任务队列中的微任务,目前有两个9和10,按照先后循序,先打印9后打印10;
打印11、12:
setTimeout为0秒比setImmediate执行早,按照先后循序,先打印11后打印12;
打印13:
setTimeout为300ms的函数,打印13;

例子:

async function async1() {
   
    console.log('2')
    //会等待await执行完 但是不会向下执行 因为下面输入微任务
    await async2()
    console.log('9')
  }

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

  console.log('1')

  setTimeout(function () {
   
    console.log('11')
    setTimeout(() => {
   
        console.log('11-1');
    },100);
    setImmediate(() => {
   
        console.log('11-2')
    })
  }, 0)

  setTimeout(function () {
   
    console.log('13')
    setTimeout(() => {
   
        console.log('15');
    },10);
    setImmediate(() => {
   
        console.log('14')
    })
  }, 300)
  setImmediate(() => console.log('12'));
  process.nextTick(() => console.log('7'));
  async1();

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

  new Promise(function (resolve) {
   
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
   
    console.log('10')
  })

  console.log('6')

总结:

理解不对的地方,还请各位大佬给予指正。

参考:https://www.cnblogs.com/everlose/p/12846375.html

目录
相关文章
|
JavaScript 前端开发
JS中 require 与 import 的区别
JS中 require 与 import 的区别
|
Web App开发 前端开发 JavaScript
React 之 requestIdleCallback 来了解一下
React 之 requestIdleCallback 来了解一下
1379 0
|
6月前
|
前端开发
如何使用 Promise 的 race 方法?
如何使用 Promise 的 race 方法?
358 62
|
7月前
|
运维 NoSQL Cloud Native
国内独家|阿里云首发MongoDB 8.0,性能提升“快”人一步
阿里云作为MongoDB的最佳战略合作伙伴,在国内独家发布了8.0版本,支撑广大用户进一步提升业务效率。
|
传感器 存储 自动驾驶
自动驾驶系统的示例和关键组成
【10月更文挑战第5天】本文介绍了使用SysML系统工具设计自动驾驶汽车内部组件的方法,重点在于通过内部块图(IBD)详细展示各子系统(如感知、控制、导航和动力系统)的内部结构及它们之间的交互。IBD不仅定义了各部件的接口和连接,还支持递归分解,有助于理解系统结构和设计接口,促进系统集成。通过端口、连接器等元素,IBD清晰展现了数据和物理流在部件间的通信方式。
352 1
|
10月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
292 12
ly~
|
监控 Linux API
如何评估 SDL 图形库优化的效果
SDL(Simple DirectMedia Layer)是一个免费开源的多媒体开发库,广泛用于游戏和多媒体应用。优化后的SDL在性能、图像质量、加载速度、兼容性、跨平台性和开发效率等方面均有显著提升。性能方面,优化后的SDL提高了渲染效率和播放路数;图像质量上,通过先进的渲染技术提升了图像的清晰度和色彩准确性;加载速度方面,通过减少格式转换时间加快了图像加载速度;兼容性和跨平台性得到增强,支持多种操作系统和硬件设备;开发效率方面,简洁的API设计和丰富的文档帮助开发者快速上手,减少了开发时间和成本。这些优化使SDL成为游戏开发和多媒体应用的理想选择。
ly~
533 4
|
前端开发 JavaScript 测试技术
前端小白逆袭之路:如何快速掌握前端测试技术,确保代码质量无忧!
【10月更文挑战第30天】前端开发技术迭代迅速,新手如何快速掌握前端测试以确保代码质量?本文将介绍前端测试的基础知识,包括单元测试、集成测试和端到端测试,以及常用的测试工具如Jest、Mocha、Cypress等。通过实践和学习,你也能成为前端测试高手。
531 4
|
JavaScript 前端开发 Java
VUE学习四:前端模块化,ES6和ES5如何实现模块化
这篇文章介绍了前端模块化的概念,以及如何在ES6和ES5中实现模块化,包括ES6模块化的基本用法、默认导出与混合导出、重命名export和import,以及ES6之前如何通过函数闭包和CommonJS规范实现模块化。
459 0
VUE学习四:前端模块化,ES6和ES5如何实现模块化
|
消息中间件 存储 算法
Kafka Raft集群搭建
Kafka Raft集群搭建
546 0