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

目录
相关文章
|
11月前
|
关系型数据库 Linux 数据库
PostgreSQL 入门指南:安装、配置与基本命令
本文从零开始,详细介绍如何在 Windows、Linux 和 macOS 上安装和配置 PostgreSQL,涵盖30+个实操代码示例。内容包括安装步骤、配置远程访问和用户权限、基础数据库操作命令(如创建表、插入和查询数据),以及常见问题的解决方案。通过学习,你将掌握 PostgreSQL 的基本使用方法,并为后续深入学习打下坚实基础。
12313 1
|
人工智能 搜索推荐 API
Cobalt:开源的流媒体下载工具,支持解析和下载全平台的视频、音频和图片,支持多种视频质量和格式,自动提取视频字幕
cobalt 是一款开源的流媒体下载工具,支持全平台视频、音频和图片下载,提供纯净、简洁无广告的体验
2120 9
Cobalt:开源的流媒体下载工具,支持解析和下载全平台的视频、音频和图片,支持多种视频质量和格式,自动提取视频字幕
|
自然语言处理 资源调度 并行计算
从本地部署到企业级服务:十种主流LLM推理框架的技术介绍与对比
本文深入探讨了十种主流的大语言模型(LLM)服务引擎和工具,涵盖从轻量级本地部署到高性能企业级解决方案,详细分析了它们的技术特点、优势及局限性,旨在为研究人员和工程团队提供适合不同应用场景的技术方案。内容涉及WebLLM、LM Studio、Ollama、vLLM、LightLLM、OpenLLM、HuggingFace TGI、GPT4ALL、llama.cpp及Triton Inference Server与TensorRT-LLM等。
1893 7
|
缓存 JavaScript 前端开发
拿下奇怪的前端报错(三):npm install卡住了一个钟- 从原理搞定安装的全链路问题
本文详细分析了 `npm install` 过程中可能出现的卡顿问题及解决方法,包括网络问题、Node.js 版本不兼容、缓存问题、权限问题、包冲突、过时的 npm 版本、系统资源不足和脚本问题等,并提供了相应的解决策略。同时,还介绍了开启全部日志、使用替代工具和使用 Docker 提供 Node 环境等其他处理方法。
11833 2
|
消息中间件 存储 算法
Kafka Raft集群搭建
Kafka Raft集群搭建
648 0
|
JSON JavaScript 前端开发
TypeScript(十六)配置相关(tsconfig配置)
TypeScript(十六)配置相关(tsconfig配置)
710 1
|
Ubuntu Linux 网络安全
在Linux中,如何配置VPN连接?
在Linux中,如何配置VPN连接?
|
运维 监控 负载均衡
在Linux中,有三百台服务器,如何进行管理?
在Linux中,有三百台服务器,如何进行管理?
|
存储 资源调度 JavaScript
Vue3如何使用Pinia详细介绍、pinia持久化存储(pinia-plugin-persistedstate详细配置)
Vue3如何使用Pinia详细介绍、pinia持久化存储(pinia-plugin-persistedstate详细配置)
echarts 高亮轮廓的中国地图
echarts 高亮轮廓的中国地图
812 0

热门文章

最新文章