让我了解 JavaScript 的底层工作原理
你有没有想过,当你运行一小段 JavaScript 代码时,幕后到底发生了什么?其实,我很久都没有想过。
我现在就想和你一起理解。我们一起探索吧!上车,出发!🚎
我建议编写一个非常小的代码块并尝试理解它的故事:
const totalApples = 10;
const totalBananas = 5;
function getTotalFruits() {
return totalApples + totalBananas;
}
const totalFruits = getTotalFruits();
太棒了,我们都知道这里的结果是 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();
脚本执行,全局执行上下文创建:
步骤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);
伙计们,这下有意思了。新术语又揭晓了。
你知道 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);
好的,当我们获取数据时,我们将再次与 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