热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

前端基础学习之JS执行机制内存模型(堆内存、栈内存、队列、执行栈)、事件循环

概念数据结构堆(Heap)、栈(Stack)、队列(Queue)是存储数据的数据结构或存储机制。它们对数据的操作顺序堆:进出随意。栈:先进后出后
概念

数据结构

堆(Heap)、栈(Stack)、队列(Queue)是存储数据的数据结构或存储机制。

它们对数据的操作顺序

堆:进出随意。
栈:先进后出/后出先进的压入式存储。
队列:先进先出。

后进先出 LIFO 译为 Last In First Out
也可以称为 先进后出 FILO 译为 First In Last Out
先进先出 FIFO 译为 First In Last Out

数据存储区域

JS数据存储在内存中,分为两个数据结构类型:栈内存和堆内存。

栈内存:存储的是标示符(变量)和基本数据类型。也就是 String Number null undefined Boolean Symbol和引用数据的指针(对象的指针)。
堆内存:存放引用数据类型的代码块。也就是栈中指针指向的对象的值。

JS根据垃圾回收机制对内存进行回收。
在这里插入图片描述

任务/消息队列(任务存储区域)

JS是单线程的,同步执行的,为避免代码阻塞,会将阻塞性的任务使用异步方式执行。
异步任务如setTimeout HTTP请求 Promise等。

同步任务在主线程执行。异步任务通过API在其他线程执行,执行完将回调函数放入一个任务队列。
任务队列存放的数据结构类型是 队列(Queue)

JS的单线程

执行上下文

JS代码运行前会创建执行上下文(执行环境),JS有三种执行上下文:

  1. 全局执行上下文
  2. 函数执行上下文
  3. eval执行上下文

执行栈(调用栈)

执行栈是js代码执行(方法调用)时候开辟的内存空间。
变量声明操作不会占用这个空间。

JS任务开始处理,将代码逐行压入执行栈,执行完一行就移出一行。
如果有嵌套执行上下文,就依次压入,上下文执行完同样移出(先进后出)。
任务处理完后(执行栈清空),再压入下一个任务的代码。

执行栈的数据结构是,所以采用先进后出的方式进行 执行移出

示例

console.log(1);
function foo() {console.log('foo');bar();
}
function bar() {console.log('bar');
}
foo();
console.log(4);

执行顺序:

  1. JS引擎将全部代码加载,在执行栈中压入一个匿名的调用anonymous
  2. console.log(1)压入执行栈
  3. console.log(1)执行完,移出执行栈
  4. foo()压入执行栈
  5. foo函数中有执行上下文,运行这个上下文,将console.log('foo')压入
  6. console.log('foo')执行完移出
  7. bar()压入
  8. bar函数中有执行上下文,console.log('bar')压入
  9. console.log('bar')移出
  10. bar() 移出
  11. foo()移出
  12. anonymouse移出,执行栈清空,等待下个任务。

任务

我理解的JS任务包括同步任务和异步产生的微任务宏任务

同步任务

同步运行的代码。

宏任务


#浏览器Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

微任务


#浏览器Node
process.nextTick
MutationObserver
Promise.then catch finally

async/await本质上属于对Promise的封装,所以await相当于promise.then。

示例

console.log(1);
const p = new Promise((resolve,reject)=>{console.log(2);resolve();
});
p.then(()=>{console.log(3)
});
setTimeout(()=>{console.log(4)
},0)

同步任务A:打印1;创建一个promise实例化对象p,打印2。
微任务B:执行promise.then回调,打印3。
宏任务C:执行setTimeout回调。

任务执行顺序

JS执行从同步任务开始,执行完查询微任务,微任务全部执行完,执行下一个宏任务。
一个宏任务中包含同步任务,也可能包含微任务。
同样从同步任务开始,执行完查询微任务,微任务全部执行完,执行下一个宏任务。
…依此处理

所以上例是按照A>B>C顺序执行的。

  • 微任务和宏任务都会创建一个队列。
  • 微任务先执行,宏任务后执行。
  • 微任务全部拉入执行栈,宏任务一次拉一个。

事件循环EventLoop

JS拥有一个基于事件循环的并发模型,事件循环负责执行代码收集和处理事件以及执行队列中的任务

并发:同一时间段执行多个任务,但同一时间点只能执行一个任务。如吃饭、喝水,同一时间点只能干一件事情
并行:同一时间点可以执行多个任务。如烧水、玩手机可以同时进行


  • 执行代码:运行 执行栈 中的代码。
  • 收集和处理事件:收集任务,判断任务执行顺序。
  • 执行队列中的任务:执行栈清空时,查询任务队列是否有任务,有则将下一个任务取出压入执行栈去执行。这个顺序依据 队列Queue先进先出

总结

执行栈相当于JS引擎正在执行的工作表。
队列相当于待办任务列表。
执行栈中工作执行完成,触发事件循环,从任务列表中取下一个任务执行。
当执行栈和任务队列中都没有工作要处理。JS执行完毕。

运行时的概念

可视化描述

在这里插入图片描述

栈内存 Stack

函数调用形成了一个由若干 帧(Frame) 组成的 栈(Stack)

function foo(b) {let a = 10;return a + b + 11;
}function bar(x) {let y = 3;return foo(x * y);
}console.log(bar(7)); // 返回 42

当调用 bar 时,第一个帧包含了 bar 的参数和局部变量,压入栈中。 当 bar 调用 foo 时,第二个帧被创建并被压入栈中,放在第一个帧之上,帧中包含 foo 的参数和局部变量。
开始执行后,当 foo 执行完毕然后返回时,第二个帧就被弹出栈(剩下 bar 函数的调用帧 )。当 bar 也执行完毕然后返回时,第一个帧也被弹出,栈就被清空了

队列 Queue

一个 Javascript 运行时包含了一个待处理任务的 队列(Queue) 。每一个 任务(Task) 都关联着一个用以处理这个任务的回调函数。

事件循环 期间的某个时刻,运行时会从最先进入队列的任务开始处理。被处理的任务会被移出队列,并调用与之关联的函数。

正如前面所提到的,调用一个函数总是会为其创造一个新的 栈帧

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个任务(如果还有的话)。

事件循环 Event Loop

任务队列一直在同步的等待任务执行完成,一个任务被完整的执行完(栈帧执行完,栈为空),才会继续执行下一个任务。
这个实现方式,称为 事件循环

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上,一个任务就会被添加到任务队列。
如果没有事件监听器,这个事件将会丢失。

setTimeout 描述运行时的过程

函数 setTimeout(tastFunc, delay) 接受两个参数:待加入队列的任务(一个函数)一个时间值(可选,默认为 0)。这个时间值代表了从执行setTimeout开始, 任务 被实际 加入到队列延迟时间

------------------个人理解 start--------------

这里的延迟时间,需要区分是针对 添加消息 还是 处理任务
个人理解为 添加任务&#xff0c;所以未使用参考文档中的 <最小延迟时间>作为表述

个人理解参考文档中的 最小延迟时间 &#xff0c;指的是setTimeout一经执行&#xff0c;就将任务添加到队列中&#xff0c;然后保证前面的任务执行完毕 和 延迟时间到达 两个条件后&#xff0c;再处理。

这样理解&#xff0c;等于当执行两条setTimeout时&#xff0c;任务会按照执行setTimeout顺序插入到队列中&#xff0c;根据任务队列先进先出原则无论它们的延迟时间如何&#xff0c;后面的总要等到前面的任务执行完才会执行。

可是实际确不是如此&#xff1a;

setTimeout(()&#61;>{console.log(1)}, 1000);
setTimeout(()&#61;>{console.log(2)}, 0);
// 输出&#xff1a;2 1

参考文档中的 最小 &#xff0c;可能指的是执行setTimeout之前的代码会影响 添加任务 的时间。
所以当按照下面表述时&#xff0c;使用 最小 应该是合理的&#xff1a;
时间值代表了&#xff0c;运行脚本时&#xff0c;脚本中的这条 setTimeout 将任务添加到队列的 最小延迟时间。

------------------个人理解 end--------------

任务tastFunc 在到达 延迟时间delay 后&#xff0c;被加入到 队列Queue&#xff0c;然后等待 执行栈 里的 执行完毕&#xff0c;接着等待 当前队列中前面的任务 处理完毕&#xff0c;最终才轮到自己。

function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 0);
bar()
foo();
/* 输出&#xff1a;
foo function
outside function
inside function
*/

执行顺序&#xff08;个人理解&#xff09;&#xff1a;

  1. 任务队列添加3个任务&#xff1a;setTimeout(outside,0)、bar()、foo()并按顺序处理。
  2. 处理任务1&#xff1a;setTimeout 添加 任务outside 到队列中。
  3. 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
  4. 处理任务3&#xff1a;foo 打印。
  5. 处理任务4&#xff1a;outside 打印。
  6. 处理任务5&#xff1a;inside 打印。

变更一下&#xff1a;

function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 100); // 变更了这里
bar()
foo();
/* 输出&#xff1a;
foo function
inside function
outside function
*/

执行顺序&#xff08;个人理解&#xff09;&#xff1a;

  1. 队列添加3个任务&#xff1a;setTimeout(outside,100)、bar()、foo()并按顺序处理。
  2. 处理任务1&#xff1a;setTimeout 告诉系统100毫秒后&#xff0c;添加 任务outside 到队列中。
  3. 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
  4. 处理任务3&#xff1a;foo 打印。
  5. 处理任务4&#xff1a;inside 打印。
  6. 100毫秒等待结束&#xff0c;向队列添加 任务outside。
  7. 处理任务5&#xff1a;outside 打印。

再变更一下&#xff1a;

function foo() {console.log(&#39;foo function&#39;)
}
function inside() {console.log(&#39;inside function&#39;)
}
function outside() {console.log(&#39;outside function&#39;)
}
function bar() {setTimeout(inside, 0);
}
setTimeout(outside, 1); // 再次变更了这里
bar()
foo();
/* 输出&#xff1a;
foo function
outside function
inside function
*/

执行顺序&#xff08;个人理解&#xff09;&#xff1a;

  1. 队列添加3个消息&#xff1a;setTimeout(outside,100)、bar()、foo()并按顺序处理。
  2. 处理任务1&#xff1a;setTimeout 告诉系统1毫秒后&#xff0c;添加 任务outside 到队列中。
  3. 1毫秒等待结束&#xff0c;向队列添加 任务outside。
  4. 处理任务2&#xff1a;bar 执行 setTimeout 添加 任务inside 到队列中。
  5. 处理任务3&#xff1a;foo 打印。
  6. 处理任务4&#xff1a;outside 打印。
  7. 处理任务5&#xff1a;inside 打印。

setTimeout中的延迟时间参数delay&#xff0c;定义了在向队列添加任务的延迟时间。
任务处理是在执行栈中以帧为单位&#xff0c;同步依次执行至完成。
在不同的设备中&#xff0c;1帧所定义的时间不一样&#xff0c;大致有 1/24 1/30 1/40 1/60 秒等值。
所以上面示例定义的1毫秒&#xff0c;抢在bar函数执行setTimeout之前&#xff0c;执行了操作。

多个运行时(Runtimes)通信

一个 web worker 或者一个跨域的 iframe 都有自己的执行栈、堆栈和任务队列。两个不同的 运行时(Runtimes) 只能通过 postMessage 方法进行通信。如果另一个运行时侦听 message 事件&#xff0c;则此方法会向该运行时添加消息&#xff08;任务&#xff09;。

JS具体执行步骤

主线程执行同步代码块&#xff0c;遇到定时器、Promise等异步任务时&#xff0c;会创建事件队列&#xff0c;把它们丢到队列里去&#xff0c;等主线程执行完成后&#xff0c;再回去执行队列中的task。

JS执行主要包括 同步任务和异步任务&#xff0c;这个同步任务会进入主线程中&#xff0c;最后放入执行栈中执行。
异步任务分为微任务和宏任务&#xff0c;分别创建一个队列放入队列中&#xff08;而不是栈中&#xff09;。
主线程任务执行完&#xff0c;会把微任务全部放入执行栈中执行。
微任务执行完再取一个宏任务放入执行栈执行&#xff0c;执行完后再取一个&#xff0c;直到执行完所有宏任务。

理解不足

很多文章描述事件循环时&#xff0c;只会描述执行栈、堆、队列3个区域。
执行栈和栈内存似乎是同一区域的不同工作。

参考&#xff1a;
并发模型与事件循环
微任务、宏任务与Event-Loop
浅析JS堆、栈、执行栈和EventLoop
究竟什么是异步编程&#xff1f;(关键词讲的很清楚)


推荐阅读
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • async/await 是现代 JavaScript 中非常强大的异步编程工具,可以极大地简化异步代码的编写。本文将详细介绍 async 和 await 的用法及其背后的原理。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • 【实例简介】本文详细介绍了如何在PHP中实现微信支付的退款功能,并提供了订单创建类的完整代码及调用示例。在配置过程中,需确保正确设置相关参数,特别是证书路径应根据项目实际情况进行调整。为了保证系统的安全性,存放证书的目录需要设置为可读权限。值得注意的是,普通支付操作无需证书,但在执行退款操作时必须提供证书。此外,本文还对常见的错误处理和调试技巧进行了说明,帮助开发者快速定位和解决问题。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • Python多线程编程技巧与实战应用详解 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • PHP预处理常量详解:如何定义与使用常量 ... [详细]
  • 在深入研究 UniApp 封装请求时,发现其请求 API 方法中使用了 `then` 和 `catch` 函数。通过详细分析,了解到这些函数是 Promise 对象的核心组成部分。Promise 是一种用于处理异步操作的结果的标准化方式,它提供了一种更清晰、更可控的方法来管理复杂的异步流程。本文将详细介绍 Promise 的基本概念、结构和常见应用场景,帮助开发者更好地理解和使用这一强大的工具。 ... [详细]
  • 资源管理器的基础架构包括三个核心组件:1)资源池,用于将CPU和内存等资源分配给不同的容器;2)负载组,负责承载任务并将其分配到相应的资源池;3)分类函数,用于将不同的会话映射到合适的负载组。该系统提供了两种主要的资源管理策略。 ... [详细]
  • 本文详细探讨了 jQuery 中 `ajaxSubmit` 方法的使用技巧及其应用场景。首先,介绍了如何正确引入必要的脚本文件,如 `jquery.form.js` 和 `jquery-1.8.0.min.js`。接着,通过具体示例展示了如何利用 `ajaxSubmit` 方法实现表单的异步提交,包括数据的发送、接收和处理。此外,还讨论了该方法在不同场景下的应用,如文件上传、表单验证和动态更新页面内容等,提供了丰富的代码示例和最佳实践建议。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 基于Dubbo与Zipkin的微服务调用链路监控解决方案
    本文提出了一种基于Dubbo与Zipkin的微服务调用链路监控解决方案。通过抽象配置层,支持HTTP和Kafka两种数据上报方式,实现了灵活且高效的调用链路追踪。该方案不仅提升了系统的可维护性和扩展性,还为故障排查提供了强大的支持。 ... [详细]
author-avatar
450651324_43c723
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有