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

概念

数据结构

堆(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--------------

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

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

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

可是实际确不是如此:

setTimeout(()=>{console.log(1)}, 1000);
setTimeout(()=>{console.log(2)}, 0);
// 输出:2 1

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

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

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

function foo() {
	console.log('foo function')
}
function inside() {
	console.log('inside function')
}
function outside() {
	console.log('outside function')
}
function bar() {
	setTimeout(inside, 0);
}
setTimeout(outside, 0);
bar()
foo();
/* 输出:
foo function
outside function
inside function
*/

执行顺序(个人理解):

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

变更一下:

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

执行顺序(个人理解):

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

再变更一下:

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

执行顺序(个人理解):

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

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

多个运行时(Runtimes)通信

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

JS具体执行步骤

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

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

理解不足

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

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

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值