文章目录
- js 中堆、栈、队列介绍
- js中 堆、栈、队列的区别与应用
- 拓展(宏任务与微任务)
- Web Workers
js 中堆、栈、队列介绍
- 栈(Stack)
- 概念:
- 栈是一种遵循后进先出(LIFO - Last In First Out)原则的数据结构。就像一摞盘子,最后放上去的盘子最先被拿走。在JavaScript中,函数调用栈就是一个典型的栈结构。当一个函数被调用时,它的执行上下文(包括局部变量、参数等信息)就会被压入栈中,当函数执行完毕后,这个执行上下文就会从栈顶弹出。
- 应用实例:
- 函数调用栈:
- 例如下面的代码:
- 函数调用栈:
function a() {console.log("Function a is called");b(); } function b() {console.log("Function b is called"); } a();
- 概念:
- 当
a
函数被调用时,a
的执行上下文被压入栈中。然后a
函数内部调用b
函数,此时b
的执行上下文被压入栈顶。b
函数执行完毕后,它的执行上下文从栈顶弹出,接着a
函数执行完毕,它的执行上下文也弹出栈。 - 表达式求值(如四则运算):
- 考虑一个简单的数学表达式求值器,它可以处理简单的四则运算,例如
3 + 4 * 2
。可以利用栈来实现运算符优先级的计算。先将数字3
和4
压入栈,当遇到运算符*
时,从栈顶弹出两个数字进行乘法运算,将结果再压入栈。接着遇到+
运算符,再从栈顶弹出两个数字进行加法运算。
- 堆(Heap)
- 概念:
- 堆是一种用于动态分配内存的数据结构,在JavaScript中主要用于存储对象和数组(复杂数据类型)。它是一个无序的内存区域,与栈不同,堆中的内存分配和释放是由垃圾回收机制(Garbage Collection)来管理的。JavaScript的引擎会自动分配和回收堆中的内存,程序员一般不需要手动管理堆内存。
- 应用实例:
- 对象存储:
- 例如创建一个对象:
- 对象存储:
- 概念:
let person = {name: "John",age: 30
};
- 这个
person
对象就存储在堆中。它的内存空间是在运行时动态分配的,JavaScript引擎会在堆中找到一块足够大的空间来存储这个对象的属性和值。 - 大型数组存储:
- 当创建一个大型数组,如
let largeArray = new Array(1000000);
,这个数组也是存储在堆中的。因为栈的空间相对较小,通常用于存储简单数据类型(如基本数据类型的值)和函数调用相关的信息,而像这种大型的数据结构就存储在堆中。
- 队列(Queue)
- 概念:
- 队列是一种遵循先进先出(FIFO - First In First Out)原则的数据结构。就像排队买票,先到的人先买票离开。在JavaScript中,可以用数组来模拟队列的操作。
- 应用实例:
- 任务队列(事件循环中的任务队列):
- 在JavaScript的事件循环机制中,有宏任务队列和微任务队列。例如,当浏览器加载页面时,用户触发的事件(如点击事件)会被放入宏任务队列。当执行一个异步操作(如
setTimeout
),它的回调函数也会被放入宏任务队列。当执行栈为空时,事件循环会从宏任务队列中取出一个任务执行。 - 例如:
- 在JavaScript的事件循环机制中,有宏任务队列和微任务队列。例如,当浏览器加载页面时,用户触发的事件(如点击事件)会被放入宏任务队列。当执行一个异步操作(如
- 任务队列(事件循环中的任务队列):
- 概念:
console.log("Script start");
setTimeout(() => {console.log("Timeout callback");
}, 0);
console.log("Script end");
- 在这里,
console.log("Script start")
先执行,然后setTimeout
的回调函数被放入宏任务队列,接着console.log("Script end")
执行。当执行栈为空时,从宏任务队列中取出setTimeout
的回调函数执行。 - 数据缓冲队列:
- 假设你正在开发一个网络应用,从服务器接收数据。数据可能是断断续续地到达的,你可以使用一个队列来缓冲这些数据。当接收到新的数据时,将其放入队列的尾部,当需要处理这些数据时,从队列的头部取出数据进行处理。例如:
let dataQueue = [];
function receiveData(data) {dataQueue.push(data);
}
function processData() {if (dataQueue.length > 0) {let data = dataQueue.shift();// 在这里进行数据处理,比如解析数据等操作console.log("Processing data:", data);}
}
js中 堆、栈、队列的区别与应用
- 区别
- 存储数据类型
- 栈:主要存储基本数据类型(如
number
、string
、boolean
、undefined
、null
)以及函数调用的执行上下文。这些数据的大小是固定的,并且在编译阶段就可以确定所需的内存空间。例如,一个number
类型的数据在栈中占用固定的字节数。 - 堆:用于存储复杂数据类型,也就是对象(包括普通对象、数组、函数等)。对象的大小不固定,因为其属性的数量和内容可以动态变化。比如一个包含多个属性的对象,其内存占用空间取决于属性的数量和每个属性值的大小。
- 队列:本身不用于存储特定的数据类型,而是一种数据结构模式。在JavaScript实现中,通常可以用数组来存储各种类型的数据,无论是基本数据类型还是对象,重点在于按照先进先出的规则操作这些数据。
- 栈:主要存储基本数据类型(如
- 内存管理方式
- 栈:内存的分配和释放是自动的,遵循后进先出原则。当一个函数被调用时,其相关的局部变量等信息被压入栈,函数执行结束后,这些信息就会从栈顶自动弹出,内存空间被释放。这种自动管理机制简单高效,但栈的空间相对较小。
- 堆:内存的分配是动态的,由JavaScript引擎的垃圾回收机制(Garbage Collection,GC)来管理。当对象不再被引用时,垃圾回收器会在适当的时候回收其占用的内存。由于堆的内存空间较大,但管理相对复杂,垃圾回收过程可能会对性能产生一定影响。
- 队列:在内存管理方面没有特殊的机制,主要依赖于所使用的存储结构(如数组)自身的内存管理。如果用数组来模拟队列,数组的内存分配和释放遵循JavaScript中数组的规则。
- 数据访问方式
- 栈:数据的访问遵循后进先出原则,只能从栈顶进行插入(压栈)和删除(出栈)操作。就像一个只有一个开口的容器,最后放入的东西最先被取出。
- 堆:对象在堆中的存储位置是通过引用(指针)来访问的。一个变量(存储在栈中)保存了对象在堆中的内存地址,通过这个引用可以访问和操作对象的属性。访问对象的属性是通过解引用的方式,从存储引用的变量找到堆中的对象。
- 队列:按照先进先出的规则访问数据,从队列头部取出数据,从队列尾部插入数据。可以将其想象成一个管道,数据从一端进入,从另一端出去。
- 存储数据类型
- 应用
- 栈的应用
- 函数调用和递归:在函数调用过程中,栈用于存储函数的执行上下文,包括局部变量、参数、返回地址等信息。递归函数的执行也依赖于栈,每一次递归调用都会将新的执行上下文压入栈中。例如,计算阶乘的递归函数:
function factorial(n) {if (n === 0 || n === 1) {return 1;}return n * factorial(n - 1);
}
- 表达式求值:可以利用栈来实现算术表达式求值,特别是对于包含括号和不同优先级运算符的表达式。例如,将中缀表达式转换为后缀表达式并求值的过程中,栈可以用于存储运算符和操作数,按照运算符的优先级和括号规则进行计算。
- 堆的应用
- 对象和数组操作:在JavaScript中,几乎所有的对象和数组都存储在堆中。这包括创建和操作DOM节点(在浏览器环境中)、处理复杂的数据结构如树形结构(如HTML文档的DOM树)、存储和管理大量的数据集合(如数据库查询结果以对象数组形式存储)。
- 动态内存分配:当需要在运行时动态创建大量的数据结构,如游戏中的角色属性对象、图形绘制中的图形对象等,这些对象的内存分配都在堆中进行。垃圾回收机制确保了在对象不再使用时释放内存,防止内存泄漏。
- 队列的应用
- 任务队列和事件循环:在JavaScript的事件驱动编程模型中,任务队列(宏任务队列和微任务队列)是核心概念。例如,
setTimeout
和setInterval
的回调函数、用户触发的事件处理函数(如点击事件、键盘事件等)都被放入任务队列中,等待执行栈空闲后按照先进先出的顺序执行。 - 消息队列和异步处理:在一些异步编程场景中,如Web Workers(在浏览器中实现后台线程处理)或者Node.js中的异步I/O操作,消息队列可以用于传递和处理异步任务的结果。比如,一个Web Worker完成计算任务后,将结果通过消息队列发送回主线程进行处理。
- 任务队列和事件循环:在JavaScript的事件驱动编程模型中,任务队列(宏任务队列和微任务队列)是核心概念。例如,
- 对象和数组操作:在JavaScript中,几乎所有的对象和数组都存储在堆中。这包括创建和操作DOM节点(在浏览器环境中)、处理复杂的数据结构如树形结构(如HTML文档的DOM树)、存储和管理大量的数据集合(如数据库查询结果以对象数组形式存储)。
拓展(宏任务与微任务)
- 概念
- 宏任务队列(Macrotask Queue)
- 宏任务队列是一个用于存储宏任务的队列。宏任务是指那些比较大型、执行时间相对较长的任务。在浏览器环境或者Node.js环境中,常见的宏任务包括
setTimeout
、setInterval
、I/O
操作(如读取文件、网络请求)、DOM
操作(如添加或删除节点)等。这些任务按照先进先出(FIFO)的原则排队等待执行。 - 事件循环(Event Loop)会不断检查调用栈是否为空,当调用栈为空时,它会从宏任务队列中取出一个宏任务放入调用栈中执行。例如,当设置一个
setTimeout
函数时,其回调函数就会被放入宏任务队列中,等待合适的时机执行。
- 宏任务队列是一个用于存储宏任务的队列。宏任务是指那些比较大型、执行时间相对较长的任务。在浏览器环境或者Node.js环境中,常见的宏任务包括
- 微任务队列(Microtask Queue)
- 微任务队列用于存储微任务。微任务通常是一些比较小的、执行速度相对较快的任务。在JavaScript中,常见的微任务包括
Promise
的then
/catch
/finally
回调、MutationObserver
(用于观察DOM变化)回调、queueMicrotask
函数(用于手动添加微任务)等。 - 微任务队列的优先级高于宏任务队列。当一个宏任务执行完毕后,在执行下一个宏任务之前,事件循环会先检查微任务队列是否为空。如果微任务队列中有任务,就会依次执行这些微任务,直到微任务队列清空后,才会从宏任务队列中取出下一个宏任务执行。
- 微任务队列用于存储微任务。微任务通常是一些比较小的、执行速度相对较快的任务。在JavaScript中,常见的微任务包括
- 宏任务队列(Macrotask Queue)
- 工作流程示例
- 假设我们有以下代码:
console.log('Script start');
setTimeout(() => {console.log('SetTimeout callback');
}, 0);
Promise.resolve().then(() => {console.log('Promise then callback');
});
console.log('Script end');
- 执行过程如下:
- 首先执行
console.log('Script start')
,输出Script start
。 - 遇到
setTimeout
,其回调函数被放入宏任务队列。 - 遇到
Promise.resolve().then()
,其回调函数被放入微任务队列。 - 执行
console.log('Script end')
,输出Script end
。 - 此时当前宏任务(主代码块)执行完毕,事件循环检查微任务队列,发现有任务,执行
Promise
的then
回调函数,输出Promise then callback
。 - 微任务队列清空后,事件循环从宏任务队列中取出
setTimeout
的回调函数并执行,输出SetTimeout callback
。
- 首先执行
- 应用场景
- 微任务队列应用场景
- 异步操作的顺序保证:
Promise
广泛用于处理异步操作,通过微任务队列可以确保Promise
的后续操作(then
、catch
、finally
)按照正确的顺序执行。例如,在一系列连续的Promise
操作中,后一个Promise
的then
回调可以在前一个Promise
完成后立即执行,保证了异步操作的连贯性。 - DOM更新的优化:
MutationObserver
利用微任务队列来处理DOM的变化观察。当DOM发生变化时,MutationObserver
的回调函数作为微任务执行。这使得DOM更新可以在当前宏任务执行完所有同步操作之后、下一个宏任务开始之前进行处理,避免了不必要的DOM重绘和回流,提高了性能。
- 异步操作的顺序保证:
- 宏任务队列应用场景
- 延迟执行和定时任务:
setTimeout
和setInterval
是典型的宏任务应用,用于在指定的延迟时间后执行任务或者周期性地执行任务。例如,在网页中实现一个定时刷新数据的功能,或者在一定延迟后显示一个提示信息,都可以使用setTimeout
将相应的任务放入宏任务队列。 - I/O操作和长时间任务:在Node.js中,读取文件、网络请求等I/O操作通常作为宏任务。这些任务可能需要较长的时间来完成,将它们放入宏任务队列可以让其他任务(如处理用户输入、执行一些快速的计算等)在等待I/O完成的过程中继续执行,提高系统的整体效率。
- 延迟执行和定时任务:
- 微任务队列应用场景
Web Workers
-
Web Workers概念
- Web Workers是HTML5提供的一种在后台线程中运行脚本的机制。在传统的JavaScript编程中,所有的脚本都在单线程(主线程)中运行,这意味着如果一个任务需要花费很长时间,如复杂的计算或者大量的数据处理,会阻塞主线程,导致页面无响应(例如,浏览器的UI冻结,用户无法进行交互操作)。Web Workers允许你将一些耗时的任务放在独立于主线程的后台线程中执行,这样主线程可以继续响应用户的操作,从而提高页面的性能和用户体验。
- Web Workers通过消息传递机制与主线程进行通信。它不能直接访问DOM元素,因为DOM操作不是线程安全的,多个线程同时操作DOM可能会导致不可预测的错误。每个Web Worker都有自己的全局对象(
self
),这个全局对象与主线程的window
对象不同,它没有document
、window
等与DOM相关的属性。
-
应用场景
- 复杂计算任务
- 在一些需要进行大量数学计算的场景中,如数据加密/解密、图形渲染中的数学计算(例如3D图形的光线追踪算法)、金融数据计算(如风险评估模型)等。这些计算可能会花费大量时间,如果在主线程中进行,会导致页面卡顿。使用Web Workers可以将这些计算任务放在后台线程进行,不影响主线程的正常运行。
- 数据处理任务
- 当需要处理大量的数据,如大数据集的排序、过滤、分析等。例如,在一个数据分析Web应用中,用户上传一个包含大量数据点的文件,需要对这些数据进行统计分析。可以使用Web Workers在后台处理这些数据,同时主线程可以更新进度条或者响应用户的其他操作。
- 多任务并发处理
- 对于一些可以并行处理的任务,比如同时从多个不同的服务器获取数据(如多图加载)或者同时进行多个独立的计算任务。通过创建多个Web Workers,可以并发地处理这些任务,加快整体任务的完成速度。
- 复杂计算任务
-
实例
- 计算斐波那契数列(复杂计算任务)
- 主线程代码(
index.html
):
- 主线程代码(
- 计算斐波那契数列(复杂计算任务)
<!DOCTYPE html>
<html>
<head><title>Web Workers Fibonacci Example</title>
</head>
<body><p>计算斐波那契数列</p><input type="number" id="inputNumber" placeholder="输入斐波那契数列的项数"><button id="calculateButton">计算</button><div id="result"></div><script>const calculateButton = document.getElementById('calculateButton');const resultDiv = document.getElementById('result');const inputNumber = document.getElementById('inputNumber');calculateButton.addEventListener('click', function () {const worker = new Worker('worker.js');const n = parseInt(inputNumber.value);worker.addEventListener('message', function (event) {resultDiv.textContent = '斐波那契数列的第' + n +'项是:' + event.data;});worker.postMessage(n);});</script>
</body>
</html>
- Web Worker代码(
worker.js
):
self.addEventListener('message', function (e) {const n = e.data;function fibonacci(n) {if (n === 0 || n === 1) {return n;}let a = 0, b = 1, temp;for (let i = 2; i <= n; i++) {temp = a + b;a = b;b = temp;}return b;}self.postMessage(fibonacci(n));
});
- 在这个例子中,当用户点击“计算”按钮时,主线程创建一个Web Worker,并将用户输入的斐波那契数列的项数发送给Web Worker。Web Worker在后台线程中计算斐波那契数列的指定项,计算完成后将结果发送回主线程。主线程收到结果后,将其显示在页面上。整个计算过程不会阻塞主线程,用户可以在计算过程中继续与页面进行交互。