热门标签 | 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;(关键词讲的很清楚)


推荐阅读
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Commit1ced2a7433ea8937a1b260ea65d708f32ca7c95eintroduceda+Clonetraitboundtom ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 不同优化算法的比较分析及实验验证
    本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • 本文介绍了UVALive6575题目Odd and Even Zeroes的解法,使用了数位dp和找规律的方法。阶乘的定义和性质被介绍,并给出了一些例子。其中,部分阶乘的尾零个数为奇数,部分为偶数。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 有没有一种方法可以在不继承UIAlertController的子类或不涉及UIAlertActions的情况下 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
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社区 版权所有