10分钟理解JS堆、栈以及事件循环的概念

  • 时间:2018-06-17 21:01 作者:小菜鸟程序员 来源:小菜鸟程序员 阅读:348
  • 扫一扫,手机访问
摘要:其实一开始对栈、堆的概念特别模糊,只知道如同跟内存有关,又如同事件循环也沾一点边。面试薄荷的时候,面试官正好也问到了这个问题,当时只可以大方的承认不会。痛定思痛,回去好好的研究一番。我们将从JS的内存机制以及事件机制和大量的(例子)来理解栈、堆到底是个什么玩意。概念比较多,不使用死读,所有的心里想一

其实一开始对栈、堆的概念特别模糊,只知道如同跟内存有关,又如同事件循环也沾一点边。面试薄荷的时候,面试官正好也问到了这个问题,当时只可以大方的承认不会。痛定思痛,回去好好的研究一番。

我们将从JS的内存机制以及事件机制和大量的(例子)来理解栈、堆到底是个什么玩意。概念比较多,不使用死读,所有的心里想一遍,浏览器console看一遍就很清楚了。

对前台有兴趣或者者正在学习web前台的小伙伴,能私信小编【学习】就可免费领取2018最新的一整套系统web前台学习教程!

JS内存机制

由于JavaScript具备自动垃圾回收机制,所以对于前台开发来说,内存空间并不是一个经常被提及的概念,很容易被大家忽视。特别是很多不专业的朋友在进入到前台之后,会对内存空间的认知比较模糊。

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

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

pre class="brush:js;toolbar:false"> Number String Null Undefined Boolean (es6新引入了一种数据类型,Symbol)

最简单的

pre class="brush:js;toolbar:false">var a = 1

我们定义一个变量a,系统自动分配存储空间。我们能直接操作保存在栈内存空间的值,因而基础数据类型都是按值访问。

数据在栈内存中的存储与用方式相似于数据结构中的堆栈数据结构,遵循后进先出的准则。

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

堆内存的

pre class="brush:js;toolbar:false">var b = { xi : 20 }

与其余语言不同,JS的引使用数据类型,比方数组Array,它们值的大小是不固定的。引使用数据类型的值是保存在堆内存中的对象。JavaScript不允许直接访问堆内存中的位置,因而我们不可以直接操作对象的堆内存空间。看一下下面的图,加深了解。

比较

?

10分钟理解JS堆、栈以及事件循环的概念

pre class="brush:js;toolbar:false">var a1 = 0; // 栈 var a2 = 'this is string'; // 栈var a3 = null; // 栈var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中

因而当我们要访问堆内存中的引使用数据类型时,实际上我们首先是从栈中获取了该对象的地址引使用(或者者地址指针),而后再从堆内存中获得我们需要的数据。

测试

pre class="brush:js;toolbar:false">var a = 20;var b = a;b = 30;console.log(a)var m = { a: 10, b: 20 }var n = m;n.a = 15;console.log(m.a)

同学们自己在console里打一遍,再结合下面的图例,就很好了解了

10分钟理解JS堆、栈以及事件循环的概念

10分钟理解JS堆、栈以及事件循环的概念

内存机制我们理解了,又引出一个新的问题,栈里只可以存基础数据类型吗,我们经常使用的function存在哪里呢?

浏览器的事件机制

一个经常被搬上面试题的

pre class="brush:js;toolbar:false">console.log(1)let promise = new Promise(function(resolve,reject){ console.log(3) resolve(100)}).then(function(data){ console.log(100)})setTimeout(function(){ console.log(4);})console.log(2)

上面这个demo的结果值是 1 3 2 100 4

10分钟理解JS堆、栈以及事件循环的概念

对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可可以会调少量Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走**(注意:栈里的代码是先进后出)**,走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。

  • 1.所有同步任务都在主线程上执行,形成一个执行栈
  • 2.主线程之外,还存在一个任务队列。只需异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
  • 4.主线程从任务队列中读取事件,这个过程是循环不断的

概念又臭又长,没关系,我们先粗略的扫一眼,接着往下看。

举一个说明栈的执行方式

pre class="brush:js;toolbar:false">var a = "aa";function one(){ let a = 1; two(); function two(){ let b = 2; three(); function three(){ console.log(b) } }}console.log(a);one();

demo的结果是 aa 2

图解

10分钟理解JS堆、栈以及事件循环的概念

执行栈里面最先放的是全局作使用域(代码执行有一个全局文本的环境),而后再放one, one执行再把two放进来,two执行再把three放进来,一层叠一层。

最先走的一定是three,由于two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,而后是two出,再是one出。

那队列又是怎样一回事呢?

再举一个

pre class="brush:js;toolbar:false">console.log(1);console.log(2);setTimeout(function(){ console.log(3);})setTimeout(function(){ console.log(4);})console.log(5);

首先执行了栈里的代码,1 2 5。 前面说到的settimeout会被放在队列里,当栈执行完了之后,从队列里增加到栈里执行(此时是依次执行),得到 3 4

再再举一个

pre class="brush:js;toolbar:false">console.log(1);console.log(2);setTimeout(function(){ console.log(3); setTimeout(function(){ console.log(6); })})setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); })})console.log(5)

同样,先执行栈里的同步代码 1 2 5. 再同样,最外层的settimeout会放在队列里,当栈里面执行完成以后,放在栈中执行,3 4。 而嵌套的2个settimeout,会放在一个新的队列中,去执行 6 7.

再再再看一个

pre class="brush:js;toolbar:false">console.log(1);console.log(2);setTimeout(function(){ console.log(3); setTimeout(function(){ console.log(6); })},400)setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); })},100)console.log(5)

如上:这里的顺序是1,2,5,4,7,3,6。也就是只需两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(由于只有当时间到了的时候,才会把set放到队列里面去)

pre class="brush:js;toolbar:false">setTimeout(function(){ console.log('setTimeout')},0)for(var i = 0;i<10;i++){ console.log(i)}

这个demo的结果是 0 1 2 3 4 5 6 7 8 9 setTimeout

所以,得出结论,永远都是栈里的代码先行执行,再从队列中依次读事件,加入栈中执行

stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调使用了WebAPIs里的异步方法,那这些异步方法会在再被调使用的时候放在队列里,而后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。

再回到我们的第一个

pre class="brush:js;toolbar:false">console.log(1)let promise = new Promise(function(resolve,reject){ console.log(3) resolve(100)}).then(function(data){ console.log(100)})setTimeout(function(){ console.log(4);})console.log(2)

上面这个demo的结果值是 1 3 2 100 4

  • 为什么setTimeout要在Promise.then之后执行呢?
  • 为什么new Promise又在console.log(2)之前执行呢?

setTimeout是宏任务,而Promise.then是微任务 这里的new Promise()是同步的,所以是立即执行的。

这就要引入一个新的话题宏任务和微任务(面试也会经常提及到)

宏任务和微任务

参考 Tasks, microtasks, queues and schedules(https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly)

概念:微任务和宏任务都是属于队列,而不是放在栈中

一个新的

pre class="brush:js;toolbar:false">console.log('1');setTimeout(function() { console.log('setTimeout');}, 0);Promise.resolve().then(function() { console.log('promise1');}).then(function() { console.log('promise2');});console.log('2');

1 2 promise1 promise2 setTimeout

宏任务(task)

浏览器为了可以够使得JS内部宏任务与DOM任务可以够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…) 鼠标点击会触发一个事件回调,需要执行一个宏任务,而后解析HTMl。但是,setTimeout不一样setTimeout的作使用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打印‘setTimeout’在‘promise1 , promise2’之后。由于打印‘promise1 , promise2’是第一个宏任务里面的事情,而‘setTimeout’是另一个新的独立的任务里面打印的。

微任务 (Microtasks)

微任务通常来说就是需要在当前 task 执行结束后立即执行的任务 比方对一系列动作做出反馈,或者者是需要异步的执行任务而又不需要分配一个新的 task,这样便能减小一点性可以的开销。只需执行栈中没有其余的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。假如在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调

一旦一个pormise有了结果,或者者早已有了结果(有了结果是指这个promise到了fulfilled或者rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即便这个promise早已有了结果。所以对一个已经有了结果的**promise调使用.then()**会立即产生一个微任务。这就是为什么‘promise1’,'promise2’会打印在‘script end’之后,由于所有微任务执行的时候,当前执行栈的代码必需已经执行完毕。‘promise1’,'promise2’会打印在‘setTimeout’之前是由于所有微任务总会在下一个宏任务之前一律执行完毕。

还是

pre class="brush:js;toolbar:false">
// elementsvar outer = document.querySelector('.outer');var inner = document.querySelector('.inner');//监听element属性变化new MutationObserver(function() { console.log('mutate');}).observe(outer, { attributes: true});// click listener…function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random());}// inner.addEventListener('click', onClick);outer.addEventListener('click', onClick);

click promise mutate click promise mutate (2) timeout

很好的解释了,setTimeout会在微任务(Promise.then、MutationObserver.observe)执行完成之后,加入一个新的宏任务中

多看少量

pre class="brush:js;toolbar:false">console.log(1);setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log('promise1') })})setTimeout(function(){ console.log(3) Promise.resolve(1).then(function(){ console.log('promise2') })})setTimeout(function(){ console.log(4) Promise.resolve(1).then(function(){ console.log('promise3') })})

1 2 promise1 3 promise2 4 promise3

pre class="brush:js;toolbar:false">console.log(1);setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log('promise1') setTimeout(function(){ console.log(3) Promise.resolve(1).then(function(){ console.log('promise2') }) }) })})

1 2 promise1 3 promise2

对前台有兴趣或者者正在学习web前台的小伙伴,能私信小编【学习】就可免费领取2018最新的一整套系统web前台学习教程!

  • 全部评论(0)
最新发布的资讯信息
【系统环境|】你是HTML 5的一员吗?(2019-08-15 11:58)
【系统环境|】Web前端基础怎么学?html、css、JavaScript 知识架构图(2019-08-14 17:55)
【系统环境|】零基础小白走Web前端之路是否可行?答案是yes!(2019-08-13 11:44)
【系统环境|】你知道Web前端与HTML5技术的区别吗?(2019-08-12 14:10)
【系统环境|】移动前端开发和web前端开发的区别(2019-08-11 16:49)
【系统环境|】给新手的锦囊:Web前端开发小白的学习建议和路线图(2019-08-10 16:34)
【系统环境|】都说web前端开发薪资高,入行就有上万月薪,转行难吗?(2019-08-09 14:55)
【系统环境|】干货教程 | Web前端开发学习入门指南(2019-08-08 18:27)
【系统环境|】前端开发:vue路由之前端路由的原理(2019-08-07 13:17)
【系统环境|】你不知道的web前端那些事(2019-08-06 12:29)
手机二维码手机访问领取大礼包
返回顶部