JavaScript 是怎么运行起来的?

今天我就带大家来一起回顾一下,JavaScript 的真正的工作原理,里面不涉及深入的源码解析,只是希望能够用最简单的描述让大家弄明白整个过程,主要分为下面几个部分:

  • 解释型和编译型语言

  • JavaScript 引擎

  • EcmaScript 和 JavaScript 引擎的关系

  • 运行时环境

  • 为啥是单线程

  • 调用堆栈的执行过程

  • JavaScript 语言的解析过程

解释型和编译型语言


大家可能之前都听说过,JavaScript 是一种解释型的编程语言,那么啥叫解释型语言呢?

编程语言是用来写代码的,代码是给人看的。计算机只看得懂机器代码(01010101),看不懂语言代码。将我们能看得懂的代码转换为计算机可读的机器代码有两种方式:解释和编译。

e5593f38029f5c5047149b661b9cb9f9.png

编译型语言

编译型语言直接可以转换为计算机处理器可以执行的机器代码,运行编译型语言需要一个 “构建” 的步骤,每次更新了代码你也要重新 “构建” 。

它们会比解释语言更快更高效地执行。也可以更好的控制硬件,例如内存管理和 CPU 使用率。但是,在完成整个编译的步骤需要花费额外的时间,生成的二进制代码对平台有一定的依赖性。

常见的编译型语言有 C、C ++、Erlang、Haskell、Rust 和 Go。

解释型语言

解释型语言 是通过一个解释器逐行解释并执行程序的每个命令。

因为在运行时翻译代码的过程增加了开销,解释型语言曾经比编译型语言慢很多。但是,随着即时编译的发展,这种差距正在缩小。

但是,解释型语言更灵活一点,并且一般都能动态植入,程序也比较小。另外,因为是通过解释器自己执行源程序代码的,所以代码本身相对于平台是独立的。

常见的解释型语言有 PHP、Ruby、Python 和 JavaScript。

最后再来看看,谁来编译?谁来解释?谁来执行?

  • 编译型:编译器来编译,系统执行。

  • 解释型:解释器解释并执行。

JavaScript 引擎


JavaScript 是一种解释型的编程语言,所以源代码在执行之前没有被编译成二进制代码。那么计算机是怎么理解和执行纯文本脚本的呢?

这就是 JavaScript 引擎的工作,也就是我们上面提到的解释器

JavaScript 引擎是一个执行 JavaScript 代码的计算机程序。基本上所有现代浏览器都内置了 JavaScript 引擎。当我们的浏览器中加载到 JavaScript 文件时,JavaScript 引擎会从上到下解析(将其转换为机器码)并执行文件的每一行。

e22f17d5ed8c0ef6a5515a0020b8d2f3.png

每个浏览器都有自己的 JavaScript 引擎,其中最著名的引擎是 GoogleV8

Google ChromeNode.jsJavaScript 引擎都是 V8。下面还有一些其他的常见引擎:

  • SpiderMonkey:由 Firefox 开发,第一款 JavaScript 引擎,用于Firefox

  • Chakra:由微软开发,用于 Microsoft Edge

  • JavaScriptCore:由苹果开发,用于 webkit 型浏览器,比如 Safari

所有的 JavaScript 引擎都会包含一个调用栈和一个堆:

f77214364f1d7aa5896e5855eec53078.png

  • 内存堆 - 这是内存分配发生的地方,是一个非结构化的内存池,它存储我们应用程序需要的所有对象。

  • 调用堆栈 - 是我们的代码实际执行的地方

EcmaScript 和 JavaScript 引擎的关系


ECMAScript 指的是 JavaScript 的语言标准及语言版本,比如 ES6 表示语言(标准)的第 6 版。它由一个推动 JavaScript 发展的委员会制定,这个委员会指的是技术委员会( Technical Committee )第 39 号,我们一般简称 TC39,由各个主流浏览器厂商的代表以及一些互联网大厂构成。

JavaScript 引擎的核心就是实现 ECMAScript 标准,此外还提供一些额外的机制(例如 V8 提供的垃圾回收器)。

一些最新的 ECMAScript 提案,到达 stage3stage4 后,就会被 JavaScript 引擎实现,例如 v8 会把它的一些对语言标准的实现更新在它的博客上:https://v8.dev/

运行时环境


JavaScript 引擎并不能孤立运行,它需要一个好的运行时环境才能发挥更大的作用,例如 Node.js 就是一个 JavaScript 运行时环境,各种浏览器也是 JavaScript 的运行时环境。

这些运行时环境往往会提供诸如:事件处理网络请求 API回调队列或消息队列事件循环 这样的附加能力。

9e02df6207b33d2dac7b1c9f2e7a6e56.png

那么 JavaScript 引擎怎么配合这些能力在运行时环境中发挥作用呢?我们拿 Chrome 来举个例子。

0862bd6e8ef7cf19b502f594142ee038.png

Chrome 是一个多进程的架构,我们打开一个浏览器时会启动多个不同的进程协助浏览器将页面为我们呈现出来:

  • 浏览器进程:浏览器最核心的进程,负责管理各个标签页的创建和销毁、页面显示和功能(前进,后退,收藏等)、网络资源的管理,下载等。

  • 插件进程:负责每个第三方插件的使用,每个第三方插件使用时候都会创建一个对应的进程、这可以避免第三方插件crash影响整个浏览器、也方便使用沙盒模型隔离插件进程,提高浏览器稳定性。

  • GPU进程:负责3D绘制和硬件加速

  • 渲染进程:浏览器会为每个窗口分配一个渲染进程、也就是我们常说的浏览器内核,这可以避免单个 page crash 影响整个浏览器。

我们常说的浏览器内核,比如 webkit 内核,就是浏览器的渲染进程,从接收下载文件后再到呈现整个页面的过程,由浏览器渲染进程负责。浏览器内核是多线程的,在内核控制下各线程相互配合以保持同步,一个浏览器内核通常由以下常驻线程组成:

  • GUI 渲染线程:负责渲染浏览器界面 HTML 元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。

  • 定时触发器线程:浏览器定时计数器并不是由 JavaScript 引擎计数的, 因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

  • 事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如鼠标点击、AJAX 异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

  • 异步http请求线程:XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。

  • JavaScript 引擎线程:解释和执行 JavaScript 代码。

GUI 渲染线程与 JavaScript 引擎为互斥的关系,当 JavaScript 引擎执行时 GUI 线程会被挂起, GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

JavaScript 是一种单线程编程语言,所以在浏览器内核中只有一个 JavaScript 引擎线程。

但是,在 JavaScript 的一个运行环境中,因为可能有多个渲染进程,所以可能有多个 JavaScript 引擎线程。

为啥是单线程


那么,为什么 JavaScript 不设计成多个线程呢?这样不是效率更高?

作为浏览器脚本语言, JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生, JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

那么既然 JavaScript 本身被设计为单线程,为何还会有像 WebWorker 这样的多线程 API 呢?我们来看一下 WebWorker 的核心特点就明白了:

  • 创建 Worker 时, JS 引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作 DOM

  • JS 引擎线程与 Worker 线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以 WebWorker 并不违背 JS引擎是单线程的 这一初衷,其主要用途是用来减轻 cpu 密集型计算类逻辑的负担。

在单线程上运行代码非常容易,你不必处理多线程环境中出现的复杂场景 — 例如死锁。

调用堆栈的执行过程


JavaScript 是一种单线程编程语言,这意味着它有一个调用堆栈,一次只能做一件事。

调用堆栈是一种数据结构,它基本上记录了我们在程序中的位置。如果我们执行一个函数,它放会放在栈顶。如果我们从一个函数返回,其会从栈顶弹出,这就是调用堆栈的执行过程。下面这个动图很好的解释了整个运行过程:

20d5a46ada1fcff21ee908086b362f22.gif

调用堆栈中的每个条目被称为 堆栈帧。当调用堆栈中的一个 堆栈帧 需要大量时间才能被处理时,就会产生卡顿,因为浏览器没法做其他事情了。

JavaScript 代码的执行过程


我们从宏观上看到了 JavaScript 调用堆栈是怎么执行的,那么具体到每段代码上是怎么解析执行的呢?

下面我们就以 V8 为例,来看看一段 JavaScript 代码的解析执行过程。

616ef25b538eef6a53362ff188ea041f.png

上面的图展示了 V8 大体的工作流程,画的很复杂,我们简化一下,其实核心模块是下面三个:

  • 解析器(Parser):负责将 JavaScript 代码转换成 AST 抽象语法树。

  • 解释器(Ignition):负责将 AST 转换为字节码,并收集编译器需要的优化编译信息。

  • 编译器(TurboFan):利用解释器收集到的信息,将字节码转换为优化的机器码。

在执行 JavaScript 代码时,首先解析器会将源码解析为 AST 抽象语法树,解释器会将 AST 转换为字节码,一边解释一边执行。然后编译器根据解释器的反馈信息,优化并编译字节码,最后生成优化的机器码,这就是 V8 大体的工作流程。

词法分析和语法分析

我们常常提到的词法分析和语法分析的过程就是发生在解析器(Parser)执行阶段。

词法分析就是将字符序列转换为标记(token)序列的过程。

所谓 token ,就是源文件中不可再进一步分割的一串字符,类似于英语中单词,或汉语中的词。

一般来说程序语言中的 token 有:常数(整数、小数、字符、字符串等),操作符(算术操作符、比较操作符、逻辑操作符),分隔符(逗号、分号、括号等),保留字,标识符(变量名、函数名、类名等)等。

比如下面这段代码:

const 公众号 = ‘code秘密花园’;

经过词法分析后,会被转换为下面这些 token

  • const(保留字)

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
时减轻大家的负担。**

[外链图片转存中…(img-xnyHxcNm-1714897926232)]

[外链图片转存中…(img-j8KYUC7V-1714897926233)]

[外链图片转存中…(img-J1qZohO5-1714897926233)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值