前言
其實一開始對棧、堆的概念特别模糊,隻知道好像跟記憶體有關,又好像事件循環也沾一點邊。面試薄荷的時候,面試官正好也問到了這個問題,當時隻能大方的承認不會。痛定思痛,回去好好的研究一番。
我們将從
JS的記憶體機制
以及
事件機制
和
大量的(例子)
來了解棧、堆究竟是個什麼玩意。概念比較多,不用死讀,所有的心裡想一遍,浏覽器console看一遍就很清楚了。
let's go
JS記憶體機制
因為JavaScript具有自動垃圾回收機制,是以對于前端開發來說,記憶體空間并不是一個經常被提及的概念,很容易被大家忽視。特别是很多不專業的朋友在進入到前端之後,會對記憶體空間的認知比較模糊。
在JS中,每一個資料都需要一個記憶體空間。記憶體空間又被分為兩種,棧記憶體(stack)與堆記憶體(heap)。
棧記憶體一般儲存基礎資料類型
Number String Null Undefined Boolean
(es6新引入了一種資料類型,Symbol)
最簡單的
var a = 1
我們定義一個變量a,系統自動配置設定存儲空間。我們可以直接操作儲存在棧記憶體空間的值,是以基礎資料類型都是按值通路。
資料在棧記憶體中的存儲與使用方式類似于資料結構中的堆棧資料結構,遵循後進先出的原則。
堆記憶體一般儲存引用資料類型
堆記憶體的
var b = { xi : 20 }
與其他語言不同,JS的引用資料類型,比如數組Array,它們值的大小是不固定的。引用資料類型的值是儲存在堆記憶體中的對象。JavaScript不允許直接通路堆記憶體中的位置,是以我們不能直接操作對象的堆記憶體空間。看一下下面的圖,加深了解。
比較


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] 作為對象存在于堆記憶體中
是以當我們要通路堆記憶體中的引用資料類型時,實際上我們首先是從棧中擷取了該對象的位址引用(或者位址指針),然後再從堆記憶體中取得我們需要的資料。
測試
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a)
var a = 20;
var b = a;
b = 30;
console.log(a)
同學們自己在console裡打一遍,再結合下面的圖例,就很好了解了
記憶體機制我們了解了,又引出一個新的問題,棧裡隻能存基礎資料類型嗎,我們經常用的function存在哪裡呢?
浏覽器的事件機制
一個經常被搬上面試題的
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
對象放在heap(堆)裡,常見的基礎類型和函數放在stack(棧)裡,函數執行的時候在棧裡執行。棧裡函數執行的時候可能會調一些Dom操作,ajax操作和setTimeout定時器,這時候要等stack(棧)裡面的所有程式先走**(注意:棧裡的代碼是先進後出)**,走完後再走WebAPIs,WebAPIs執行後的結果放在callback queue(回調的隊列裡,注意:隊列裡的代碼先放進去的先執行),也就是當棧裡面的程式走完之後,再從任務隊列中讀取事件,将隊列中的事件放到執行棧中依次執行,這個過程是循環不斷的。
- 1.所有同步任務都在主線程上執行,形成一個執行棧
- 2.主線程之外,還存在一個任務隊列。隻要異步任務有了運作結果,就在任務隊列之中放置一個事件。
- 3.一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務隊列,将隊列中的事件放到執行棧中依次執行
- 4.主線程從任務隊列中讀取事件,這個過程是循環不斷的
概念又臭又長,沒關系,我們先粗略的掃一眼,接着往下看。
舉一個說明棧的執行方式
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
圖解
執行棧裡面最先放的是全局作用域(代碼執行有一個全局文本的環境),然後再放one, one執行再把two放進來,two執行再把three放進來,一層疊一層。
最先走的肯定是three,因為two要是先銷毀了,那three的代碼b就拿不到了,是以是先進後出(先進的後出),是以,three最先出,然後是two出,再是one出。
那隊列又是怎麼一回事呢?
再舉一個
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
再再舉一個
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.
再再再看一個
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放到隊列裡面去)
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)執行完後又将從任務隊列中依次讀取事件,這個過程是循環不斷的。
再回到我們的第一個
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)
概念:微任務和宏任務都是屬于隊列,而不是放在棧中
一個新的
console.log('1');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('2');
promise1
promise2
宏任務(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’之前是因為所有微任務總會在下一個宏任務之前全部執行完畢。
還是
<div class="outer">
<div class="inner"></div>
</div>
// elements
var 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
(2) timeout
很好的解釋了,setTimeout會在微任務(Promise.then、MutationObserver.observe)執行完成之後,加入一個新的宏任務中
多看一些
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
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')
})
})
})
})
總結回顧
- 棧:
- 存儲基礎資料類型
- 按值通路
- 存儲的值大小固定
- 由系統自動配置設定記憶體空間
- 空間小,運作效率高
- 先進後出,後進先出
- 棧中的DOM,ajax,setTimeout會依次進入到隊列中,當棧中代碼執行完畢後,再将隊列中的事件放到執行棧中依次執行。
- 微任務和宏任務
- 堆:
- 存儲引用資料類型
- 按引用通路
- 存儲的值大小不定,可動态調整
- 主要用來存放對象
- 空間大,但是運作效率相對較低
- 無序存儲,可根據引用直接擷取
廣而告之
本文釋出于
薄荷前端周刊,歡迎Watch & Star ,轉載請注明出處。
歡迎讨論,點個贊再走吧 。◕‿◕。 ~
原文釋出時間為:2018年06月11日
原文作者:薄荷前端
本文來源:
掘金 https://juejin.im/entry/5b3a29f95188256228041f46如需轉載請聯系原作者