深入理解 JavaScript 的内存管理、事件循环和消息队列(图文讲解)

首先,献祭一张图:并发模型与事件循环 - JavaScript | MDN

一、先来一个例子

不知道上图有多少人能够看懂,不过不要紧,今天我们就一次性讲清楚。

我尽量用最少的代码,来解释这些概念。

请大家看下下面的代码,来思考一下js的内存分布以及事件循环。

javascript 复制代码
        var globalVar = "全局对象";
        function outerFunction() {
            var outerVar = "外部函数变量";
            var newUser = { name: "蒙多", age: 25 };
            function innerFunction() {
                var innerVar = "内部函数变量";
                setTimeout(function () {
                    console.log("1s计时器结束");
                }, 1000);
                document.getElementById("myButton").addEventListener("click", function () {
                    console.log(newUser.name);
                });
                Promise.resolve().then(function () {
                    console.log("微任务结束");
                });
            }
            innerFunction();
        }
        outerFunction();
        var newUser = { name: "艾希", age: 25 };
        console.log(newUser.name);

二、内存管理

重点提示:栈中操作的单元是"栈帧",每个栈帧对应独立的上下文。

内存布局

JavaScript 的内存布局主要包括堆和栈。栈内存用于存储函数调用帧和原始类型数据,而堆内存用于存储对象和复杂数据结构。

我们先绘制第一部分的代码内存布局:

在上图中,栈包含多个调用帧,堆中存储着多个对象。js运行时负责压栈操作。

内存布局主要涉及栈和堆两部分:

  • :用于存储函数调用帧(Call Stack),包括函数的局部变量和原始类型数据。即是运行时空间,又是存储空间。
  • :用于存储引用类型的数据,如对象和函数。

栈帧

每次调用一个函数时,会在栈上创建一个新的栈帧,包含该函数的局部变量、参数和返回地址。引用类型的数据存储在堆中,并通过栈中的引用进行访问。

每一帧都是一个作用域或者上下文(scope),可以在chrome浏览器上看到:

每一个栈帧中,作用于里面复杂类型都是引用类型,基础类型都是copy;

对于在同一个栈中的所有帧,都可以进行 "时光倒流" 。对于同一个栈,可以回退到栈底方向的任意一帧。(有消息队列的时候不行)。

对于纯函数来说,这是一个非常重要的调试技巧。

闭包的形成

栈帧中,从下至上是global->outerFuntion->innerFunction,当一个函数(栈帧)返回的对象是一个函数的时候,就形成了一个闭包。

修改最开始的代码:

ini 复制代码
        var globalVar = "全局对象";
        function outerFunction() {
            var outerVar = "外部函数变量";
            function innerFunction() {
                var innerVar = "内部函数变量";
                console.log(outerVar);
            }
            return innerFunction;
        }
        outerFunction()();

当前程序中,当我们执行闭包的时候,会发现栈帧列表变成了 global->innerFunction。

如果按照没有闭包的话,全局作用于是无法直接访问到innerFunction的。

闭包的优劣势

闭包可以缩短调用的层级;

闭包可以访问已经被移除栈的outerFunction的内部对象,保留对象的引用关系;

闭包在当前程序中,并未引起内存泄漏,但是,在大型项目中,由于运行时间较长,结构复杂,很难把控闭包中引用关系的清除。

防漏

JavaScript引擎通过垃圾回收机制自动管理内存。垃圾回收器会定期检查堆中的对象,回收不再使用的对象所占用的内存。常见的垃圾回收算法包括:

标记-清除(Mark-and-Sweep):

  1. 垃圾回收器从根对象(如全局对象、当前执行上下文中的局部变量)开始,递归标记所有可达对象。
  2. 未被标记的对象被视为垃圾,内存被回收。

分代回收(Generational Collection):

  1. 堆内存分为新生代和老年代。
  2. 新生代存储生命周期短的对象,老年代存储生命周期长的对象。
  3. 垃圾回收器对新生代进行频繁回收,对老年代进行较少的回收。

防漏方案

根据上面的一通分析,我们可以关注与EventLoop以及闭包相关的内存泄漏问题。

对于闭包,需要谨慎使用,因为闭包会保留对其外部函数作用域的引用,这可能导致内存泄漏。具体来说,如果闭包中的引用没有被正确管理,或者引用了大量不必要的数据,就会导致内存泄漏。

对于setTimeoutsetInterval 或事件监听器,即使相关的对象已经不再需要,它们也无法被回收,从而导致内存泄漏。一定要记得移除。

三、事件循环与消息队列

JavaScript 是单线程的,这意味着它一次只能执行一个任务。事件循环负责管理任务的执行顺序,而消息队列则存储待处理的任务。

我们先绘制一下任务调用的图例:

在上图中,当调用栈执行主任务结束后,事件循环开始检查消息队列中的任务,并将任务推送到调用栈中执行。

对于事件循环来说,由于是"异步"的,所以会很难把控已经执行过的栈对堆内存的影响,对于 "时光倒流" 来说不可控,因此,无法倒流到主任务的栈帧上。

为了进一步研究,我们将调用时序进行简化,只保留事件循环、消息队列、调用栈

事件触发机制

事件可以由用户操作(如点击、输入)或其他线程(如计时器、网络请求)触发,但事件的处理始终在 JavaScript 单线程中进行。

谁控制消息队列与事件循环

JavaScript 的运行时环境(如浏览器或 Node.js)负责维护消息队列和执行事件循环。消息队列由运行时环境中的一个或多个线程维护,但执行事件循环的线程是单独的 JavaScript 线程。

宏任务微任务

上面的示例,我们忽略了这两者,因为他们都是消息队列的一种类型。废话不多说,直接上图:

用户事件

还记得有一个绑定用户点击的事件回调么?这个不属于js引擎核心部分,这里属于浏览器与js交流部分,与计时器、Promise一样,它也会触发EventLoop。这也解释了,为什么主程序已经执行结束了,还能再次调用堆栈进行运行。

用户事件也会放入消息队列中。如果说它属于什么类型的任务,应该算是"宏任务"(除了微任务,就是宏任务)。

事件循环:微任务

事件循环:宏任务

事件循环:用户输入

总结

你可能已经忘记最上面的一张图长什么样子了,我们来还原一下。

上面的例子我已经尽量做到精简并且保留主要核心概念。

理解 JavaScript 的内存管理、事件循环和消息队列是编写高效代码的关键。通过可视化和示例代码,我们可以更清晰地理解这些概念,并在实际开发中应用这些知识来优化应用程序的性能和响应速度。

在 JavaScript 中,内存管理涉及堆和栈的分配与释放。复杂数据结构如对象和数组存储在堆内存中,而原始数据类型和函数调用帧则存储在栈内存中。全局对象通常存储在堆内存中,确保在整个应用程序生命周期内都可访问。

事件循环与消息队列的关系决定了 JavaScript 如何处理异步操作。运行时环境(如浏览器或 Node.js)负责维护消息队列,并在适当的时机将回调函数推送到调用栈中执行。事件触发机制可以来自用户操作或其他线程,但事件处理始终在 JavaScript 的单线程中进行。

相关推荐
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte5 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT066 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
剪刀石头布啊6 小时前
生成随机数,Math.random的使用
前端