本文概述
虽然可能需要对C ++和C等语言有深入的了解才能编写完整的生产代码, 但是编写JavaScript时通常只能基本了解该语言可以做什么。
诸如将回调传递给函数或编写异步代码之类的概念通常并不那么容易实现, 这使得大多数JavaScript开发人员都不太在意后台发生的事情。他们只是不在乎理解语言所带来的复杂性。
作为一名JavaScript开发人员, 了解幕后真正发生的事情以及从我们这里抽象出来的大多数复杂性如何真正发挥作用变得越来越重要。它可以帮助我们做出更明智的决策, 进而可以极大地提高代码性能。
本文重点介绍JavaScript中非常重要但很少了解的概念或术语之一。事件循环!
用JavaScript不能避免编写异步代码, 但是为什么异步运行的代码真的意味着什么?即事件循环
在我们了解事件循环如何工作之前, 我们首先必须了解JavaScript本身是什么以及它如何工作!
什么是JavaScript?
在继续之前, 我希望我们先退一步。什么是JavaScript?我们可以将JavaScript定义为:
JavaScript是一种高级, 解释性, 单线程非阻塞, 异步并发语言。
等一下这是什么书呆子的定义? ????
让我们分解一下!
关于本文, 此处的关键字是单线程, 非阻塞, 并发和异步的。
单线
执行线程是可以由调度程序独立管理的已编程指令的最小序列。编程语言是单线程的, 意味着它一次只能执行一项任务或操作。这意味着它将自始至终执行整个过程, 而不会中断或停止线程。
与多线程语言不同, 在多线程语言中, 多个进程可以同时在多个线程上运行而不会互相阻塞。
JavaScript如何同时是单线程和非阻塞的?
但是阻塞是什么意思?
不阻塞
没有关于阻塞的定义。它仅表示线程中运行缓慢的事物。因此, 非阻塞意味着线程上的运行不会变慢。
但是, 等等, 我是否说过JavaScript在单个线程上运行?我还说过它是非阻塞的, 这意味着任务可以在调用堆栈上快速运行?但是如何??当我们运行计时器怎么样?循环?
放松!我们会在????中找出。
同时
并发意味着代码由多个线程并发执行。
好的, 事情现在变得很奇怪, JavaScript如何才能成为单线程并同时并发?即使用多个线程执行其代码?
异步
异步编程意味着代码在事件循环中运行。进行阻止操作时, 事件开始。阻塞代码将继续运行, 而不会阻塞主执行线程。当阻止代码完成运行时, 它将阻止操作的结果排入队列, 并将其推回堆栈。
但是JavaScript只有一个线程吗?在允许线程中的其他代码执行的同时, 什么执行该阻塞代码呢?
在继续之前, 让我们回顾一下上面的内容。
- JavaScript是单线程的
- JavaScript是非阻塞的, 即缓慢的进程不会阻塞其执行
- JavaScript是并发的, 即它同时在多个线程中执行其代码
- JavaScript是异步的, 即, 它在其他地方运行阻塞代码。
但是以上内容并没有完全相加, 单线程语言如何才能成为非阻塞, 并发和异步的?
让我们再深入一点, 让我们深入到JavaScript运行时引擎V8, 也许它有一些我们不知道的隐藏线程。
V8引擎
V8引擎是用于Google用C ++编写的JavaScript的高性能, 开源Web程序集运行时引擎。大多数浏览器使用V8引擎运行JavaScript, 甚至流行的node js运行时环境也使用它。
简单来说, V8是一个C ++程序, 可以接收JavaScript代码, 对其进行编译和执行。
V8做两件事:
- 堆内存分配
- 调用堆栈执行上下文
可悲的是, 我们的猜想是错误的。 V8只有一个调用堆栈, 将调用堆栈视为线程。
一个线程===一个调用堆栈===一次执行一次。
图片–黑客正午
由于V8仅具有一个调用栈, 那么JavaScript如何在不阻塞主执行线程的情况下并发和异步运行?
让我们尝试通过编写一个简单而通用的异步代码来找出并一起分析。
JavaScript逐行运行每个代码, 一个接一个(单线程)。如预期的那样, 第一行将在此处的控制台中打印, 但是为什么最后一行在超时代码之前打印?为什么执行过程在继续运行最后一行之前不等待超时代码(阻塞)?
其他一些线程似乎已经帮助我们执行了该超时, 因为我们可以肯定某个线程在任何时间点只能执行一个任务。
让我们先来窥视一下V8源代码。
等等…什么?? !!! V8中没有计时器功能, 没有DOM?没有活动吗?没有AJAX吗? Yeeeeessss !!!
事件, DOM, 计时器等不是JavaScript核心实现的一部分, JavaScript严格符合Ecma脚本规范, 并且其各种版本通常根据其Ecma脚本规范(ES X)进行引用。
执行工作流程
事件, 计时器, Ajax请求都由浏览器在客户端提供, 通常称为Web API。它们使单线程JavaScript成为非阻塞, 并发和异步的!但是如何?
任何JavaScript程序的执行工作流程分为三个主要部分, 分别是调用堆栈, Web API和任务队列。
调用堆栈
堆栈是一种数据结构, 其中最后添加的元素始终是要从堆栈中删除的第一个元素, 你可以将其视为板的堆栈, 其中只有最后添加的第一个板可以首先删除。调用堆栈不过是堆栈数据结构而已, 在堆栈数据结构中相应地执行任务或代码。
让我们考虑以下示例;
当你调用函数printSquare()时, 该函数被压入调用堆栈, printSquare()函数将调用square()函数。 square()函数被压入堆栈, 还调用multiple()函数。乘法功能被压入堆栈。由于乘法函数返回并且是最后被压入堆栈的对象, 因此首先对其进行解析, 然后将其从堆栈中删除, 然后是square()函数, 然后是printSquare()函数。
Web API
在这里执行V8引擎未处理的代码, 而不是”阻塞”主执行线程。当调用堆栈遇到Web API函数时, 该过程将立即移交给正在执行的Web API, 并释放调用堆栈以在执行过程中执行其他操作。
让我们回到上面的setTimeout示例;
当我们运行代码时, 第一个console.log行被压入堆栈, 并且几乎立即获得输出, 到超时时, 计时器由浏览器处理, 并且不属于V8核心实现, 它被压入而是将其释放到Web API, 从而释放堆栈, 以便它可以执行其他操作。
当超时仍在运行时, 堆栈将前进到下一行操作并运行最后的console.log, 这说明了为什么我们在定时器输出之前将其输出。一旦计时器完成, 就会发生一些事情。然后, console.log登录, 然后计时器神奇地再次出现在调用堆栈中!
怎么样?
事件循环
在讨论事件循环之前, 让我们首先了解任务队列的功能。
回到我们的超时示例, Web API一旦完成执行任务, 就不仅仅是将其自动推回”调用堆栈”。它进入任务队列。
队列是一种按照先进先出原理工作的数据结构, 因此当任务被推入队列时, 它们以相同的顺序离开。由Web API执行的任务将被推送到任务队列, 然后返回到调用堆栈以打印出结果。
可是等等。事件循环到底是什么???
事件循环是一个过程, 它等待清除调用栈, 然后将回调从任务队列推送到调用栈。清除堆栈后, 事件循环将触发并检查任务队列中是否有可用的回调。如果有的话, 它将其推送到调用堆栈, 等待再次清除调用堆栈, 然后重复相同的过程。
上图演示了事件循环和任务队列之间的基本工作流程。
总结
尽管这是一个非常基础的介绍, 但是JavaScript中的异步编程的概念提供了足够的洞察力, 可以清楚地了解幕后情况以及JavaScript如何能够仅通过一个线程就可以并行和异步运行。
JavaScript始终是按需提供的, 如果你想学习, 我建议你阅读此Udemy课程。
评论前必须登录!
注册