让我了解 JavaScript 的底层工作原理

2025-05-24

让我了解 JavaScript 的底层工作原理

你有没有想过,当你运行一小段 JavaScript 代码时,幕后到底发生了什么?其实,我很久都没有想过。

我现在就想和你一起理解。我们一起探索吧!上车,出发!🚎

我建议编写一个非常小的代码块并尝试理解它的故事:

const totalApples = 10;
const totalBananas = 5;

function getTotalFruits() {
   return totalApples + totalBananas;
}

const totalFruits = getTotalFruits();
Enter fullscreen mode Exit fullscreen mode

太棒了,我们都知道这里的结果是 15。这么简单。但是幕后发生了什么呢?

在我们开始之前,请记住 JavaScript 是单线程同步语言

  • 它一次只能执行一件事。
  • 它从上到下逐行执行它们。

首先, 全局执行上下文是由JavaScript 引擎创建的。


JavaScript 引擎

Javascript 引擎是执行 javascript 代码的程序。

执行上下文

想象一下:执行上下文是一个双面的地方,JS 代码在这里被声明和执行。想象一下一块有两面的棋盘。

在右侧(内存),声明了所有变量和函数。

在左侧(执行线程),代码逐行执行。

执行上下文有两种类型:

全局执行上下文(GEC)

每当 JS 引擎执行脚本时,它都会创建一个全局执行上下文,所有全局代码都会处理该上下文。

函数执行上下文(FEC)

每当调用一个函数时,它都会创建一个具有自己的本地内存和执行线程的函数执行上下文。

让我们试着想象一下:

记住我们的代码:

// Step 1
const totalApples = 10;
const totalBananas = 5;

// Step 2
function getTotalFruits() {
   return totalApples + totalBananas;
}

// Step 3
const totalFruits = getTotalFruits();
Enter fullscreen mode Exit fullscreen mode

脚本执行,全局执行上下文创建:

步骤1:

在内存中声明 totalApples 和 totalBananas 变量。

图片描述

第 2 步:

在内存中声明 getTotalFruits 函数。

图片描述

你知道函数也可以保存在内存中吗?我们继续。

步骤 3(第 1 部分):

在内存中声明一个 totalFruits 变量。但是我知道这个变量的值吗?不知道!我们无法在内存中保存命令。所以它目前会被保存为未初始化状态。看看这个变量:

图片描述

步骤 3(第 2 部分):

是时候在执行线程上执行我们的函数了。函数执行上下文 (Function Execution Context) 首次出现在这里。如下所示:

图片描述

就这些吗?不,我的朋友!这里少了点什么。这一步揭示了一个新术语。每当调用一个函数时,它都会弹出到调用栈中。

调用堆栈

调用栈是 JavaScript 引擎跟踪代码执行进度的地方。
你可以在调用栈中看到当前正在运行的函数。

🎤 想象一下你是一名歌手。播音员正在喊你的名字。你上台演唱。歌曲结束后,你走下舞台。舞台就是调用栈。🎤

当一个函数被调用时,它会弹出到调用栈。当它完成其工作后,它会弹出。

全局执行默认在调用堆栈的底部运行。

函数调用:

图片描述

函数完成其工作:

图片描述

请注意:由于 JavaScript 是单线程语言,因此只有一个调用堆栈。


哈!我猜完成了,对吧?如果我做错了什么,请提醒我。我们继续努力吧。

这样看起来还不错,但是,如果我们给我们的功能增加一些复杂性会怎么样?

我们不要害怕,行动起来吧!


当同步运行时,它看起来很干净。但是当某些函数是异步时会发生什么?

我们基本上可以用 setTimeout 来实现

const totalApples = 10;
const totalBananas = 5;

function getTotalFruits() {
   return totalApples + totalBananas;
}

function runTimer() {
   console.log('timeout run!')
}

const totalFruits = getTotalFruits();

setTimeout(runTimer, 0);

console.log(totalFruits);
Enter fullscreen mode Exit fullscreen mode

伙计们,这下有意思了。新术语又揭晓了。
你知道 JavaScript 没有计时器吗?控制台也没有?

每个人有时都需要支持,Javascript 也不例外。WEB API在这里为 JavaScript 提供帮助。

Web API

Web API 通常与 JavaScript 配合使用,为我们的功能增添一些功能。计时器、控制台、网络请求以及许多其他功能都是 Web API 的技能,而不是 JavaScript 的。JavaScript
只是通过一些标签(例如 setTimeout)与浏览器进行通信。

让我们想象一下

假设在执行线程中轮到 setTimeout 了,让我们试着形象化一下这个步骤:

图片描述

嗯,奇怪的事情发生了。我来总结一下。
请注意:

函数和变量照常保存在内存中。它们在线程中执行。轮到 setTimeout 时,JavaScript 不会将 setTimeout 的回调 (runTimer) 发送到调用栈。而是与 Web 浏览器通信并发送如下消息:

嘿,伙计👋你能帮我启动一个计时器吗?计时器完成后,这是你运行的回调(runTimer)。

如您所见,0ms 后,计时器完成,回调准备运行,但是,执行线程永远不会停止,就像生活一样。

在 Web 浏览器处理计时器时,函数会继续在线程中执行,即使回调 (runTimer) 已准备好执行,调用堆栈也不再为空!记住:一次只能执行一个函数。但我手头有两个函数要运行。

会发生什么?JavaScript 是不是坏了?!
拜托,我们需要它。

或者 JavaScript 会以随机顺序运行我的函数?
那将非常难以预测,而且会带来灾难。

应该有更好的解决办法。

🚨 新学期提醒 🚨

我们的回调正在回调队列中等待。

回调队列

回调队列就像一个等候室,用于存放已准备好执行的回调。回调在这里等待调用栈清空。每当调用栈清空时,我们的回调就会弹出并执行它的任务。

事件循环

事件循环基本上会持续检查调用栈,当调用栈为空时,就会通知它。队列中的第一个回调会弹出到调用栈中。

让我们添加一些视觉效果,包括回调队列和事件循环

图片描述

哦,伙计们,胜利了!🏆

一个小问题:如果当前执行持续了 1000 秒或更长时间,而我们的回调仍在回调队列中等待,该怎么办?它会永远等待下去吗?

听起来难以置信,但确实如此!无论代价如何,它都会等到调用栈清空。除非调用栈清空,否则没有权限运行。

好了!这段代码的冒险到这里就结束了,对吧?不过我觉得我们还可以在这里增加一些复杂性。


请不要离开我!我保证这是最后一章。你不会后悔的,耐心点!


如果我需要从服务器获取数据会发生什么?

 让我们获取一些数据:

const totalApples = 10;
const totalBananas = 5;

function getTotalFruits() {
   return totalApples + totalBananas;
}

function runTimer() {
   console.log('timeout run!')
}

function showData(data) {
   console.log(data)
}

const totalFruits = getTotalFruits();

setTimeout(runTimer, 0);

// Fetch some data
const fetchData = fetch('https://jsonplaceholder.typicode.com/todos/1');

fetchData.then(showData);

console.log(totalFruits);
Enter fullscreen mode Exit fullscreen mode

好的,当我们获取数据时,我们将再次与 Web 浏览器通信。因为我们在这里需要它的 API。

我不会提及获取 API 的细节,只讨论获取数据时执行线程、调用堆栈和队列中发生的情况。

图片描述

嗯,让我们尝试写下这里发生的事情的时间表:

1)函数和变量保存在内存中,在线程中执行blablabla……

2) setTimeout 与 Web 浏览器的计时器进行通信。

3) setTimeout 的回调 (runTimer) 已准备就绪,并在 0ms 后在回调队列中等待。[0ms]

4) fetch 正在执行两项工作:
a)与 Web 浏览器的网络功能进行通信。b
)通过 Javascript 引擎在内存中创建一个特殊的 Promise 对象。

5) fetch 的回调 (showData) 已准备好数据,将在 200 毫秒后运行,我们还不知道它现在在哪里等待。我们拭目以待。[200 毫秒]

6)当所有这些发生时,console.log(15) 会弹出调用栈。假设:它花了 250 毫秒到达控制台,250 毫秒后会从调用栈弹出。(我知道实际操作中不会花这么长时间,但这只是为了理解情况而做的假设。) [ 250 毫秒]

那么,console.log(15) 在调用栈中运行,runTimer 在回调队列中等待。那么showData在哪里等待呢?

根据我们目前的知识,showData应该在回调队列中紧跟在runTimer后面等待,对吗?

不,朋友,不是这样的。
最后一次,🚨新术语提醒🚨:

除了回调队列之外,这里还有另一种队列。

微任务队列

每当回调与 JavaScript 引擎中的 Promise 对象相关时,它将在微任务队列中等待,例如 fetch 函数的回调。

注意: Microtask Queue 比 Callback Queue 有特权。

首先,微任务队列中的回调会弹出到调用栈。当它们执行完毕后,回调队列中的回调才会弹出到调用栈。

在我们的场景中,首先,showData会运行,因为它在 Microtask Queue 中等待。然后,在 Callback Queue 中等待的runTimer会运行。

那么,最终结果如下:

图片描述


真是场冒险!😓 只是一段代码而已,疯了吧。
我们的旅程到此结束,伙计们。🚎 谢谢你们的加入!

与我保持联系,了解更多冒险经历👇


关注我:

Github: https://github.com/inancakduvan/
Twitter: https: //twitter.com/InancAkduvan

谢谢你陪我读到最后!🙂

文章来源:https://dev.to/inancakduvan/let-me-understand-how-javascript-works-under-the-hood-3ibf
PREV
Kubernetes Docker 弃用了?等等,Docker 在 Kubernetes 中已经弃用了?我该怎么办?
NEXT
实践 Web 开发的资源