简介Javascript是一种怪异的语言,有人喜欢,有人讨厌。它有许多独特的机制,这些机制在其他流行语言中不存在或没有,特别是代码执行的顺序。了解浏览器环境,它是由什么构成的,它是如何工作的,会让我们在写JS的时候更有信心,为可能出现的潜在问题做好充分的准备。在这篇文章中,我们试图解释在Chrome浏览器中发生了什么,让我们来看看:V8Javascript引擎编译步骤、堆和内存管理、调用堆栈。浏览器运行时并发模型、事件循环、阻塞和非阻塞代码。JavaScript引擎最流行的JavaScript引擎是V8,它是用C++编写的,被基于Chrome的浏览器(如Chrome、Opera甚至Edge)使用。基本上,这个引擎是一个将JS转换为机器代码并在计算机的中央处理器(CPU)上执行结果的程序。编译当浏览器加载JS文件时,V8的解析器将其转换为抽象语法树(AST)。这棵树被生成字节码的解释器使用。字节码是机器代码的抽象,可以通过编译为非优化的机器代码来执行。V8在主线程中进行,而优化编译器TurboFan在另一个线程中进行一些优化并生成优化的机器码。此管道称为即时(JIT)编译。调用堆栈JavaScript是一种单线程编程语言,只有一个调用堆栈。这意味着我们的代码是同步执行的。每当一个函数运行时,它会在任何其他代码运行之前完全运行。当V8调用JS函数时,它必须将运行时数据存储在某个地方。调用堆栈是内存中由堆栈帧组成的位置。每个堆栈帧对应一个尚未被调用的函数。堆栈结构由以下部分组成:局部变量参数参数返回地址如果我们执行一个函数,V8会将帧压入堆栈顶部。当我们从函数返回时,V8跳出框架。如上例所示,在每次函数调用时都会创建一个框架,并在每次返回语句时删除。其他所有内容都动态分配到一个称为堆的大型非结构化内存块中。堆(Heap)有时V8在编译时并不知道一个对象变量需要多少内存。此类数据的所有内存分配都发生在堆上。退出分配内存的函数后,堆上的对象继续存在。V8有一个内置的垃圾收集器(GC)。垃圾收集是内存管理的一种形式。它就像一个收集器,试图释放不再使用的对象占用的内存。换句话说,当一个变量失去所有引用时,GC会将那块内存标记为不可访问并释放它。我们可以通过在Chrome开发工具中创建快照来研究堆。每个实例化的JS对象都分组在其构造函数类下。括号中的分组表示不能直接调用的本机构造函数。可以看到有很多(编译代码)和(系统)实例,还有一些传统的JS对象,比如Math、String、Array等。浏览器运行时V8可以按照标准使用调用栈来同步执行JS。但是,我们需要渲染UI并需要处理用户与UI的交互。此外,我们需要在发出网络请求时处理用户交互,对此我们无能为力。当所有代码都同步时,我们如何实现并发?感谢浏览器引擎。浏览器引擎负责渲染带有HTML和CSS的页面。在Chrome中它被称为Blink。作为WebCore的一个分支,Blink是一个布局、渲染和文档对象模型(DOM)库。Blink是用c++实现的,它提供了DOM元素和事件、XMLHttpRequest、fetch、setTimeout、setInterval等WebAPI,可以通过JS访问。让我们考虑以下使用setTimeout(onTimeout,0)的示例:正如您所见,浏览器首先将f1()和f2()函数压入堆栈,然后执行onTimeout。那么上面的例子是如何工作的呢?并发setTimeout函数执行后,浏览器引擎立即将setTimeout回调函数放入事件表中。它是一种将注册的回调映射到事件的数据结构,在我们的例子中,onTimeout函数映射到超时事件。一旦计时器到期,在本例中我们将延迟设置为0毫秒,事件将立即触发并将onTimeout函数放入事件队列(也称为回调队列、消息队列或任务队列)。事件队列是由将来要处理的回调函数(任务)组成的数据结构。最后但同样重要的是,事件循环(一个连续运行的循环)检查调用堆栈是否为空。如果是,则执行从事件队列添加的第一个回调,从而向上移动调用堆栈。函数的处理继续进行,直到调用堆栈再次为空。然后事件循环将处理事件队列中的下一个回调(如果有的话)。注意onResolve1、onResolve2和onTimeout回调的执行顺序。阻塞和非阻塞简单的说,所有的JS代码都被认为是阻塞的。当V8忙于处理堆栈帧时,浏览器卡住了,应用程序的UI被阻塞了。用户将无法单击、导航或滚动。在V8完成其工作之前,不会处理来自网络请求的响应。想象一下,我们正在浏览器中运行的程序中解析图像。在上面的例子中,事件循环被阻塞了。它无法处理事件/作业队列中的回调,因为调用堆栈包含此帧。WebAPI为我们提供了通过异步回调编写非阻塞代码的可能性。当调用setTimeout或fetch等函数时,我们将所有工作委托给在单独线程中运行的C++本机代码。操作完成后,回调将放入事件队列。同时,V8可以继续执行JS代码。使用这种并发模型,我们可以在不阻塞JS执行线程的情况下处理网络请求、用户与UI的交互等。总结对于每个希望能够解决复杂任务的开发人员来说,理解JS环境的组成是至关重要的。现在我们知道了异步JavaScript是如何工作的,调用堆栈、事件循环、事件队列和作业队列在其并发模型中的作用。正如您可能已经猜到的那样,V8引擎和浏览器引擎的幕后工作还有很多。然而,我们大多数人只需要对所有这些概念有一个基本的了解。
