写一篇你能看懂的Node.js事件循环

事件循环

下面的两句话是Node.js官网对Node.js的简介。这两句话应该是我们看到的最多的了。

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。
Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

简短两句句话就Node.js介绍得非常清楚了。但是很多人对事件驱动和非阻塞I/O模型很模糊。
简单来说就是:

  • 事件驱动:是指Node.js把每一个任务当成事件来处理。
  • 非阻塞I/O:是指Node.js遇到I/O任务时,会从线程池调度单独的线程处理I/O操作,不会阻塞主线程。

异步非阻塞I/O

Node.js JavaScript 代码运行在单个线程上。 每次只处理一件事。
但它大大简化了并发编程的方式,我们只需要记住代码会在单个事件循环上运行,并避免任何可能阻塞线程的事情。

一般来说,高并发的解决方案就是提供多线程模型。那么 Node.js 如何通过单线程来实现高并发和异步 I/O?
答案是Node.js非常重要的事件循环

什么是事件循环

我们先来看一张很常见的图(下图),它展示了Node.js的运行原理
在这里插入图片描述
从图中我们可以看出Node.js 被分为了四层,分别是应用层、V8引擎层、Node API层 和 LIBUV层

  • 应用层:即JavaScript 交互层,常见的就是Node.js的模块,比如 http,fs
  • V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
  • NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
  • LIBUV层: 是跨平台的底层封装,实现了事件循环、文件操作等,是 Node.js 实现异步的核心 。

EVENT LOOP即事件循环,从上图可以看出EVENT LOOP属于LIBUV层。

事件循环过程分为如下几个步骤:

  1. Node.js在主线程里维护了一个事件队列(EVENT QUEUE)
  2. 当接收到请求后,就将该请求作为一个事件放入这个队列中
  3. 然后继续接收其他请求。
  4. 当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件。
  5. 处理事件分以下两种情况:
  • 如果事件是非I/O任务,就亲自处理,并通过回调函数返回到上层调用
  • 如果事件是I/O任务

① 从线程池中拿出一个线程处理事件,并指定回调函数

②然后继续循环队列中的其他事件

③当线程中的I/O任务完成后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环

④当主线程再次循环到该事件时,就直接处理并返回给上层调用。

通过以上事件循环过程,我们可以得知:

Node.js内部是通过LIBUV层的线程池来完成异步I/O操作的,因此,Node.js的单线程仅仅只JavaScript代码运行在单线程中,而并非Node.js是单线程,这就是为什么Node.js中JavaScript运行在单线程中,却能够通过事件驱动模型实现高并发和异步I/O。

接下来我会以代码示例来简单介绍事件循环的过程

栈溢出

栈是连续的存储结构,并且栈的结构比较小,要在内存中寻找一份连续的存储结构是不容易的。所以当调用层级很深时,调用栈内存在的函数执行上下文太多,就会造成栈溢出

比如下面的代码就会造成栈溢出

function foo() {
    foo();
}
foo();

在这里插入图片描述
代码执行过程以及调用栈结构如下图所示:
在这里插入图片描述
在 foo 函数中调用 foo 函数,就要不停地将其执行上下文压入栈中,无法结束。造成栈溢出。

下面我将介绍使用setTimeout函数解决这个栈溢出的问题,也就会用到事件循环中的消息队列

消息队列(Event Loop)

将栈溢出的代码加上setTimeout函数,代码如下:

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

在这里插入图片描述
执行后,一直打印foo,可以看到没有出现栈溢出的问题

我先将该代码在消息队列、主线程、调用栈的执行流程画下,你看执行流程,再看结论。

  1. 压入全局执行上下文
    在这里插入图片描述

  2. 执行foo函数,压入foo函数执行上下文
    在这里插入图片描述

  3. 执行setTimeout,setTimeout会将foo函数封装成一个新的宏任务,并将其添加到消息队列中,等待执行
    在这里插入图片描述

  4. foo函数执行结束,从调用栈中弹出其执行上下文,调用栈被清空
    在这里插入图片描述

  5. 主线程从消息队列中取出待执行的回调函数foo并执行
    在这里插入图片描述
    总结:
    由于事件循环赋予调用堆栈优先级,即首先处理调用堆栈中找到的所有东西,一旦其中没有任何东西,便开始处理消息队列中的东西。setTimeout的本质是将同步函数调用改为异步函数调用,即将回调函数封装成宏任务,并将其添加消息队列中,这样就不用一直占用主线程的调用栈,所以不会出现栈溢出的问题。

考察你学会了没有

那么你再看下下面的代码执行结果是什么?
自己先想想输出结果是什么,然后再看答案

var bar = () => console.log('bar')

var baz = () => console.log('baz')

var foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()

该代码打印如下:

foo
baz
bar

你明白了吗?不明白那就在看一遍上面讲的消息队列

ES6作业队列

通过上面一系列的执行流程图,我们知道了setTimeout是添加到消息队列的后面,如果消息队列中的任务过多,那可能会导致要执行的操作延迟。
那有没有办法让其任务添加到消息队列的前面呢?

ECMAScript 2015 引入了作业队列的概念,Promise 使用了该队列。 这种方式会尽快地执行异步函数。

比如下面的代码:

var bar = () => console.log('bar')

var baz = () => console.log('baz')

var foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  new Promise((resolve, reject) =>
    resolve('应该在 baz 之后、bar 之前')
  ).then(resolve => console.log(resolve))
  baz()
}

foo()

打印结果如下:

foo
baz
应该在 baz 之后、bar 之前
bar

这是 Promise(以及基于Promise 构建的 async/await)与通过 setTimeout() 或其他平台 API 的普通的旧异步函数之间的巨大区别。

参考

支持🤟

🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟

  • 我会持续编写文章,保持每周至少一篇文章。💪
  • 有时候编写一篇文章需要大量时间。💪
  • 您只需一秒即可完成【点赞👍或关注❤️】。💪
  • 您的支持将给与我更大的动力。💪

🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟🤟

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值