前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >EventLoop 系列 - 单线程、调用栈、堆、队列、Eventloop 这些概念了解下~

EventLoop 系列 - 单线程、调用栈、堆、队列、Eventloop 这些概念了解下~

作者头像
五月君
发布2021-10-18 15:27:37
9850
发布2021-10-18 15:27:37
举报
文章被收录于专栏:Nodejs技术栈

在 《JavaScript 异步编程指南》的上个模块中,我主要讲解了异步编程的基本应用,在这个模块系列中我想来聊聊事件循环,英文称为 EventLoop。

相信这个名字对于参加过 JavaScript 面试的同学(包括前端或后端 Node.js)而言不会陌生。

讨论事件循环的文章很多,成系列的倒不是很多见,我将事件循环放在《JavaScript 异步编程指南》系列的第二个模块展开讨论,也是希望能够对 JavaScript 异步编程有个更深刻的理解。

学习事件循环前置知识

JavaScript 这门编程语言,既可以在客户端浏览器上运行,也可以在服务端 Node.js 上运行。我想以一种自己理解的角度来讲,所以上来不会直接去讲浏览器中的 EventLoop 或 Node.js 中的 EventLoop。

事件循环中的一些概念,无论是在浏览器或 Node.js 中我们去学习事件循环时,这些都是通用的,了解这些概念对于后面的学习也会相对轻松些。

单线程、调用栈、堆、队列、Eventloop 这些词通过可视化界面描述看起来就像下图展示的,但是它们之间的关系是怎么样呢?接下来我会分别的去介绍。

为什么是单线程?

JavaScript 是单线程的,此时,是否有疑问为什么是单线程呢?多线程处理效率不是更高吗?

需要从浏览器说起,在浏览器环境中对于 DOM 操作,试想如果多个线程来对同一个 DOM 操作,一个线程添加 DOM 而另一个线程删除 DOM 那这结果到底是删除还是添加呢?是不是就乱了呢?那也就意味着对于 DOM 的操作只能是单线程,避免 DOM 渲染冲突。

在浏览器环境中 UI 渲染线程和 JavaScript 执行引擎是互斥的,一方在执行时都会导致另一方被挂起。

上面说了既然 JavaScript 是单线程的,那么同一时间只能处理一件事情,对于高并发大量请求不是会造成程序阻塞吗?

答案是 No,解决阻塞等待的方案就是异步,例如,程序发起一次网络请求或文件请求不必同步等待响应结果,真正处理这些任务由另外的线程实现,待有结果了再通知到 JavaScript 主线程,在 JavaScript 中正是通过单线程加事件循环实现的,同时也避免了多线程上下文切换,资源抢占问题,达到更好的高并发成就。

另外,HTML5 提出了 Web Worker 标准,Node.js 提供了 worker_threads 模块,允许我们在服务中创建多个线程,但是这些都没改变 JavaScript 单线程的本质,这些创建线程属于子线程还是由主线程来管理。

调用栈

栈是一种先进后出的数据结构,JavaScript 是一个单线程的编程语言,每次只能运行一段代码,有且只有一个调用栈

JavaScript 中所有的任务可以归为两种:同步任务与异步任务。

我们先看第一种,同步任务在主线程上排队执行,形成一个由若干个帧组成的调用栈(Call Stack)

下例,当调用 hello() 函数时,第一个帧被创建压入栈中,该函数又调用了 intro() 函数,第二个帧被创建并压入栈中,位于 hello() 之上。此时 intro() 函数中没有在调用其它函数了,按照栈的后进先出的规则,intro() 函数开始执行直到完成第二个帧从栈中弹出,之后开始执行 hello() 函数,执行完毕之后,第一个帧从栈中弹出,栈也就被清空了。

代码语言:javascript
复制
function intro() {
 console.log('My name is codingMay!');
}
function hello() {
  intro();
 console.log('Hello');
}
hello();

通过动图的方式展示下运行结果(本身是个 Gif 动图,文件略过大微信公众号不支持)。

在开发中,还有一个问题也是不可避免的,在某些场景下程序会抛出一些错误信息,也许是显示的错误定义,也许是意外的未知错误。

我们对示例做下改造,让 intro() 抛出一个 Error 对象,在 Chrome 控制台运行之后,错误信息从 intro、Hello 再到匿名函数,把整个错误的调用栈都打印出来了。这是一个同步调用,上下文信息是有关联的,程序能够跟踪到下一行要执行的一些代码。

你可能还听过一个问题 “内存泄漏”,下面左侧就是一个例子,hello() 函数递归调用自身,代码没有设置边界,hello() -> hello() -> ... 程序一直这样运行下去,调用栈不断的增加数据,直到超过栈的最大空间限制,程序会报一个错误 VM356:4 Uncaught RangeError: Maximum call stack size exceeded

思考一个问题 “上面的递归代码怎么改造才能不触发栈溢出?前提是还是递归调用。”

JavaScript 在执行时所有的数据会存放在内存里,像函数、函数变量、参数等这些已知数据占用空间的存在于内存区域的栈中,代码执行过程中创建的对象,存在于堆中,也是内存中的另外一块区域。

队列与回调函数

在 JavaScript 中当调用栈有东西还在执行时,我们的程序也不会空闲去执行其它的操作,试想,如果调用栈出现一些很耗时的任务,如果是用在客户端用户会看到页面被卡住了,如果是用在服务端会造成接口响应很慢,也就没有并发优势了,这是很糟糕的一件事,我们不能让 JavaScript 主线程阻塞。

修改下上面的示例,在 inrto() 方法里加上 setTimeout 延迟执行,看下程序的执行是怎么样的?

代码语言:javascript
复制
function intro() {
 setTimeout(function timer() {
   console.log('My name is codingMay!');
  }, 8000)
}
function hello() {
  intro();
 console.log('Hello');
}
hello(); 

上述代码,intro() 函数内部执行了 setTimeout 定时器函数,这个是异步的,我们的 JavaScript 主线程不会在这里等待,会立即返回。setTimeout 第一个参数我们传入的 timer 这个是我们需要执行的代码,这里 timer 通常也是我们说的回调函数。

注:Web Apis 这个是由宿主环境提供的 API,这里也有单独的线程来实现,例如定时器就是由宿主环境实现的。

setTimeout 不是由 JavaScript 引擎实现的,这个是由 JavaScript 程序所运行的宿主环境提供的,理解这个概念也不难,在客户端我们的宿主环境就是浏览器,如果在服务端就是 Node.js。当计时器时间到了之后,宿主环境会将 timer 函数封装为一个事件放入 “队列”,队列是一个先进先出的数据结构

接下来执行队列里的任务就是 EventLoop 了~

EventLoop

EventLoop 从这个名字上也可以看出它是一个持续循环的过程,它会检查当前调用栈是否为空,只有在当前调用栈为空后进入下一个 Loop,如果任务队列有任务,取出执行,如果任务队列为空,它会同步地等待消息到达。

按照如下类似方式来实现:

代码语言:javascript
复制
while (queue.waitForMessage()) {
  queue.processNextMessage(); // 同步地等待消息到达
}

通过一个 Gif 完整的展示其运行效果,稍微有点灰,因为上传过程中被压缩了。

Reference

  • http://latentflip.com/loupe
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
  • https://medium.com/@sanderdebr/a-brief-explanation-of-the-javascript-engine-and-runtime-a0c27cb1a397
  • https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth

- END -

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Nodejs技术栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 学习事件循环前置知识
  • 为什么是单线程?
  • 调用栈
  • 队列与回调函数
  • EventLoop
  • Reference
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档