浅析JS堆,栈,执行栈和EventLoop

现在前端面试,大多数都会问到关于事件循环、执行栈等问题,本文通过案例,图片等形式给大家讲解这些概念,如果认真看完,我相信大部分人都能彻底的理解

JS内存机制

Javascript具有自动垃圾回收机制,周期性会检查没有使用的变量, 进行回收释放,所以在闭包中,如果引用了外部的变量,则无法进行释放和回收,一般会传参进去。

  • 垃圾回收:找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作

在JS中,每一个数据都需要一个内存空间,内存空间又分为栈内存(stack)和堆内存(heap)

栈内存一般存储基础数据类型

Number String Null Undefined Boolean Symbol
复制代码

堆内存一般存储引用数据类型

var user = {name: 'jack'}
var arr = [1, 3, 5]
复制代码

JS的引用数据类型,比如数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象,Javascript不允许直接访问内存中的位置,因此我们不能直接操作对象的堆内存空间

image.png

var num = 1; // n
var name = 'come on baby'; // 栈
// 变量user存在于栈中,{name: 'bobo'}存在于堆内存中
var user = { name: 'v' }
// 变量arr存在于栈中,[1, 2, 3]作为对象存在于堆内存中
var arr = [1, 3, 5];
复制代码

因此当我们要访问堆内存中的引用数据类型时,实际上我们首先是从栈中获取了该对象的指针,然后再从堆内存中取得我们需要的数据

所以,我们经常说:基本类型赋值相互不影响,引用类型赋值,会影响原对象

var a = 20;
var b = a;
b = 30;
// a为20,b为30,值类型不影响

console.log(a);
var user = {name: 'bobo'}
var info = user;
info.name = 'Jack'

// 打印Jack指向同一个内存地址

console.log(user.name);
复制代码

总结:

  • JavaScript具备自动垃圾回收机制
  • JS内存分为堆内存和栈内存
  • 引用类型在栈中保存指针,在堆中保存对象值
  • 栈内存数据遵循 先进后出

EventLoop

执行栈

JS代码在运行钱都会创建执行上下文,也可以理解为执行环境,JS中有三种执行上下文:

  • 全局执行上下文,默认的,在浏览器中是window对象
  • 函数执行上下文, JS的函数每当被调用时会创建一个上下文
  • Eval执行上下文,eval函数会产生自己的上下文

通常,我们的代码中都不止一个上下文,那这些上下文的执行顺序应该是怎么样的? 从上往下一次执行?
栈,是一种数据结构,遵循先进后出的原则,JS中的执行栈就具有这样的结构,当引擎遇到JS代码时,会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文。引擎执行栈顶的函数,执行完毕,弹出当前执行上下文。

function foo () {
    console.log(1);
    bar()
    console.log(3)
}
function bar() {
    console.log(2)
}
foo()
复制代码

image.png

image.png
首先执行这个JS文件,创建一个全局上下文,并压入执行栈中,当 foo() 函数被调用时,将 foo 函数的执行上下文压入执行栈,接着执行输出 ‘1’;当 bar() 函数被调用,将 bar 函数的执行上下文压入执行栈,接着执行输出 ‘2’;bar() 执行完毕,被弹出执行栈,foo() 函数接着执行,输出 ‘3’;foo() 函数执行完毕,被弹出执行栈,最后清空整个执行栈。这就是先进后出,Foo先被压入执行栈,最后才被弹出执行栈

EC就是Execute Context执行上下文

总结:

  • 所有JS代码运行,都需要放入执行栈中.
  • 执行上下文包含了三种(全局、函数、eval)
  • 栈是一种数据结构,遵循 先进后出
console.log(1)
new Promise(function(resolve){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(data)
})
setTimeout(function(){
    console.log(4);
})
console.log(2)
复制代码

[上面的面试题打印结果: 1 3 2 100 4]

你能说出具体执行步骤吗?

我们都知道JS本身是单线程的,一次只能干一件事儿,那么像定时器、Promise这些它是怎么处理的呢?实际上就要介绍quene队列了。

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

所以,我们的JS执行主要包括同步任务和异步任务,整个同步任务会进入到主线程中,最后放入执行栈中执行,就是我们上面给大家讲解的执行栈,接下来关注异步任务。

浏览器的JS中,异步任务又分为宏任务和微任务,宏任务和微任务都是属于队列,而不是放在栈中。微任务会创建一个队列,宏任务会创建一个队列,而主线程执行完以后,会优先执行微任务,把微任务全部放到执行栈中执行,最后再从宏任务中取出一个放入执行栈进行执行,执行完后,再取一个,直到执行完所有的宏任务。

接下来看张图:

image.png

左侧JS图包含了堆和栈,所有的代码都会被放入栈中执行,我们叫执行栈,执行栈是一条主线程,先执行同步任务,中间遇到ajax、setTimeout等异步任务后,会push到queue中,最后再把队列中事件取出来放入执行中执行,依次循环这个过程。

console.log(1)
new Promise(function(resolve){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(data)
})
setTimeout(function(){
    console.log(4);
})
console.log(2)
复制代码
  1. 创建全局上下文,并压入执行栈中

  2. 把同步代码console.log压入执行栈中执行,打印 1,并出栈

  3. 把同步代码new Promise压入执行栈中执行,打印 3,并出栈

注意:new Promise 这个过程实际上是同步的,只有resolve和reject后才是异步

  1. then属于异步任务,push到微任务队列中,并创建事件

  2. setTimeout属于异步任务,push到宏任务队列中,并创建事件

注意:宏任务和微任务是两个队列

  1. 把同步代码console.log压入执行栈中执行,打印 2,并出栈
  2. 微任务先执行,所以把微任务队列中的事件全部拿出来,放入执行栈进行执行。打印 100,并出栈
  3. 从宏任务队列中,只取出一个事件放入执行栈中执行,打印 4

那我们把上面例子改造一下:

console.log(1)
new Promise(function(resolve){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(data)
}).then(function(){
    console.log(200)
})
setTimeout(function(){
    console.log(4);
})
setTimeout(function(){
    console.log(5);
})
console.log(2)
复制代码

有2个then,2个setTimeout,此时学完后,您觉得应该打印多少? 答案是:1 3 2 100 200 4 5

整个文章到此结束,希望大家能够看懂!

总结

JavaScript具备自动垃圾回收机制

JS内存分为堆内存和栈内存

引用类型在栈中保存指针,在堆中保存对象值

所有JS代码运行,都需要放入执行栈中.

执行代码前,会先创建执行上下文

执行上下文包含了三种(全局、函数、eval)

同步任务先执行,异步任务放队列

微任务先执行,宏任务后执行

微任务全部拉入执行栈,宏任务一次拉一个

栈是先进后出,队列是先进先出

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享