深入理解 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 的单线程中进行。

相关推荐
万叶学编程1 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
前端李易安3 小时前
Web常见的攻击方式及防御方法
前端
PythonFun3 小时前
Python技巧:如何避免数据输入类型错误
前端·python
知否技术3 小时前
为什么nodejs成为后端开发者的新宠?
前端·后端·node.js
hakesashou3 小时前
python交互式命令时如何清除
java·前端·python
天涯学馆3 小时前
Next.js与NextAuth:身份验证实践
前端·javascript·next.js
HEX9CF3 小时前
【CTF Web】Pikachu xss之href输出 Writeup(GET请求+反射型XSS+javascript:伪协议绕过)
开发语言·前端·javascript·安全·网络安全·ecmascript·xss
ConardLi3 小时前
Chrome:新的滚动捕捉事件助你实现更丝滑的动画效果!
前端·javascript·浏览器
ConardLi4 小时前
安全赋值运算符,新的 JavaScript 提案让你告别 trycatch !
前端·javascript