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

目录
相关文章
|
7天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
4天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2451 13
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
3天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1489 14
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
1月前
|
运维 Cloud Native Devops
一线实战:运维人少,我们从 0 到 1 实践 DevOps 和云原生
上海经证科技有限公司为有效推进软件项目管理和开发工作,选择了阿里云云效作为 DevOps 解决方案。通过云效,实现了从 0 开始,到现在近百个微服务、数百条流水线与应用交付的全面覆盖,有效支撑了敏捷开发流程。
19268 29
|
1月前
|
人工智能 自然语言处理 搜索推荐
阿里云Elasticsearch AI搜索实践
本文介绍了阿里云 Elasticsearch 在AI 搜索方面的技术实践与探索。
18818 20
|
1月前
|
Rust Apache 对象存储
Apache Paimon V0.9最新进展
Apache Paimon V0.9 版本即将发布,此版本带来了多项新特性并解决了关键挑战。Paimon自2022年从Flink社区诞生以来迅速成长,已成为Apache顶级项目,并广泛应用于阿里集团内外的多家企业。
17514 13
Apache Paimon V0.9最新进展
|
5天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
330 11
|
1月前
|
存储 人工智能 前端开发
AI 网关零代码解决 AI 幻觉问题
本文主要介绍了 AI Agent 的背景,概念,探讨了 AI Agent 网关插件的使用方法,效果以及实现原理。
18696 17
|
2天前
|
算法 Java
JAVA并发编程系列(8)CountDownLatch核心原理
面试中的编程题目“模拟拼团”,我们通过使用CountDownLatch来实现多线程条件下的拼团逻辑。此外,深入解析了CountDownLatch的核心原理及其内部实现机制,特别是`await()`方法的具体工作流程。通过详细分析源码与内部结构,帮助读者更好地理解并发编程的关键概念。
|
2天前
|
SQL 监控 druid
Druid连接池学习
Druid学习笔记,使用Druid进行密码加密。参考文档:https://github.com/alibaba/druid
187 80