Loading

一文说清 JS 运行时环境(Event Loop)

在本文中,我们将介绍浏览器的 javaScript 运行时环境。我们将学习 Chrome 的JS V8 引擎如何解析代码,并了解事件循环如何帮助代码在单个线程上同步和异步运行。最后,我们将看一个常见示例,该示例可以更清楚地解释此过程的工作原理。

JS 运行时环境

当您访问网站时,您可以利用各种各样的网络浏览器进行访问,例如 Chrome, Firefox, Edge 或 Safari。每个浏览器都有一个JS 运行时环境。开发人员通过访问JS 运行时提供的各种WEB API来构建程序。

由浏览器 JS运行时环境 提供的 AJAX, DOM, BOM, Event,以及其他的 WEB API 加上 ECMAscript 形成了完整的 JavaScript(actionScript)。

在运行时环境中,还有一个解析代码的JavaScript 引擎。每个浏览器都有自己版本的JS引擎。Chrome 使用它所谓的JS V8 引擎,我将以它为例分析。

将 JS 运行时环境视为一个大容器。那么在这个大容器内还有几个较小的容器成员。当JS引擎解析代码时,它开始将代码片段放入不同的容器中。

简洁明了的 JS 运行时环境:
image

JS V8 引擎

一旦 Chrome 在网页上接收到 javaScript 代码或脚本,JS V8 引擎就会开始解析它。首先,它将检查代码语法错误。如果找不到,它将开始从上到下读取代码。它的最终目标是将 javaScript 代码编译为计算机可以理解的机器代码。但是,在我们了解它对代码的确切作用之前,我们必须了解解析它的内部环境。

V8 引擎内的堆

环境中的第一个容器,组成 V8 引擎的一部分,被称为内存堆。它的主要作用是存储声明的变量声明的函数

V8 引擎内的栈

环境中的第二个容器,也是组成 V8 引擎的另一部分,被称为执行栈(调用栈)。栈是一种LIFO(last input first output)后进先出的数据结构。

只有栈顶的函数会被处理执行,除非前一个函数(处理完毕)被弹出栈,否则JS引擎不会去处理下一个函数。它的主要作用是将函数调用之类的可执行单元推入调用栈。

当函数被推入执行栈,JS 引擎就开始解析函数体,在堆里存储此函数内部声明的变量、把新的函数调用推入栈顶,或是分发到WEB API容器

当函数有了返回值,或者被分发到 WEB API 容器,它就会被弹出栈,同时下一个函数调用会被推入栈顶。

如果 JS 引擎执行完一个函数,并且该函数没有明确的指明返回值,JS 引擎会默认的返回undefined然后再将之弹出栈。人们通常说的 JS 同步运行指的就是 JS 引擎解析函数然后弹出栈(再运行下一个函数)的运行流程。简言之,在单线程下同一时间只做一件事。

WEB API容器

环境中的第三个容器。从栈发送到 WEB API 容器内的 WEB API 调用(比如事件监听处理函数、HTTP/AJAX 请求、或者是 Timers 定时器函数)会一直在 WEB API 容器内,直到触发操作为止。这些触发操作可能是一个点击事件被触发、或者是 HTTP 请求完成从数据源获取数据、或者是定时器达到触发的时间点,一旦达到触发条件,一个回调函数就会被推入第四个也是最后一个容器回调队列

回调队列

回调队列将按添加的顺序存储所有回调函数。当调用栈函数任务为空时,它会将队列开头的回调函数发送到执行栈。当执行栈函数任务再次清空时,它将发送下一个队列首位的回调函数。回调队列是一种FIFO(first input first output)先进先出的数据结构。

事件循环

事件循环是环境内的循环机制。它的工作是持续的检测调用栈和回调队列,如果检测到调用栈为空,它就会通知回调队列把队列中的第一个回调函数推入调用栈。

调用栈和回调队列可能在某一段时间内是闲置的,但是事件循环则永不停歇的检测着这两者。只要 WEB API 容器中的事件达到了触发条件,随时都可以把回调函数添加到回调队列中去。

上面这种循环机制就是我们常讲的 JS 的异步执行。但 JS 本身是同步执行的语言,说白了就是某一个时间只干一件事。

因为 WEB API 模块可以不断的向回调队列添加回调函数,而回调队列又可以不断的把回调函数函数推入执行栈,我们可以认为JS是在异步运行。表现形式上异步,实质上还是同步。说穿了就是把一时完成不了的异步任务,放在需要及时执行的同步任务之后执行。

阻塞和非阻塞I/O

提到阻塞I/O,可以想象一个函数在被无限循环调用。当函数永不停止的执行时,它就永远不会被推出栈,因此栈内的下一个函数就会被永远阻塞无法被调用。另一种情况是一个有极其复杂的逻辑和算法的函数,它必然会花费大量的时间来执行,那么就会阻塞下一个函数的执行。上面会造成阻塞的两个场景是我们编码时需要避免的,但是相比于语言的的设计缺点,更多的阻塞来自开发者的编码错误和糟糕的代码写法风格。

常见的一个阻塞I/O的操作就是 HTTP 请求,例如向某个外部网站发送数据请求,你必须等待该网站的回应。而可能永远得不到回应,那么你的代码就会被阻塞。好在 JS 运行时环境中会处理这种情况。它把 HTTP 请求分发到 WEB API 模块,然后把请求操作弹出栈。这样当请求在 WEB API 模块内等待响应数据的时候,执行栈内的下一个函数就可以被执行。即使请求无法得到数据,程序的其他部分也可以正常执行。这就是我们所说的 JS 是一个非阻塞的异步语言

经典案例

setTImeout(function(){
    console.log('Hey, Why am I last?')
}, 0)

function sayHi(){
    console.log('Hello')
}

function sayBye(){
    console.log('Goodbye')
}

sayHi()
saybye()

在控制台执行并思考结果后,让我们来看看 JS V8 引擎处理此代码的确切方式...

  1. JS 引擎先检查代码语法错误,然后代码从顶部开始更全面地解析代码。

  2. 首先解析到一个setTimeout()函数调用,将其推入执行栈的顶部,然后将其第一个参数也就是那个函数声明存储在内存堆中。

  3. 发现setTimeout函数是 WEB API 的一种,因此就把它分发到 WEB API 容器,然后将其推出执行栈。

  4. 由于计时器设置为 0 秒,因此 WEB API 容器会立即将其匿名函数推送到回调队列。事件循环会检查执行栈以查看它是否为空,但它不是,因为后面还有俩同步任务

  5. setTimeout函数被分发到 WEB API 容器,Timer()倒计时 0 秒后将匿名函数添加到任务队列(回调函数准备就绪)。

  6. Timer()倒计时的同时,JS V8 引擎向下解析出两个函数声明,并将它们存储在内存堆中。

  7. 再往下解析出函数sayHi()函数调用,sayHi()推入执行栈顶部。sayHi()函数内部调用console.log(),又将console.log()推入执行栈的顶部。

  8. JS引擎开始解析console.log()的函数体,它接收了一个消息去打印 "Hello" ,执行完毕,然后被推出执行栈。

  9. JS 引擎移知道sayHi()内部代码已全部执行完毕,所以sayHi()被推出执行栈。

  10. saybye()的过程与上面如出一辙。

  11. 然后一直运行的事件循环检测到执行栈为空。立刻通知任务队列,任务队列将准备就绪的匿名回调函数推送到执行栈栈顶。

  12. 解析匿名函数发现调用console.log()console.log()被推入到执行栈栈顶。

  13. console.log()执行完毕推出执行栈,紧接着匿名函数执行完毕推出执行栈。

提示:如果你复制代码在控制台执行,你会发现有一个undefined被输出,这是因为程序中所有的主函数都没有返回值,它们只是调用了console.log()函数,当console.log()方法被执行并弹出之后,解析器执行至主函数的结尾,并没有发现返回值。因此它返回undefined,然后被推出执行栈。

注意:本文中所讨论的环境是浏览器下的 JS 执行环境。虽然 Node.js 也是用 GoogleV8 引擎驱动的,但是它提供了一个完全不一样的运行时环境。Node.js 不会提供 DOM树、AJAX、以及其他的 WEB API 。但是,在 Node.js 环境下你可以安装你需要的扩展包来构建你的程序。

原文地址The Javascript Runtime Environment
原文作者:Jamie Uttariello(杰米·乌塔里洛)

posted @ 2022-07-07 19:44  mx羽林  阅读(707)  评论(0编辑  收藏  举报