10分鐘瞭解JS堆、棧以及事件循環的概念

前言

其實一開始對棧、堆的概念特別模糊,只知道好像跟內存有關,又好像事件循環也沾一點邊。面試薄荷的時候,面試官正好也問到了這個問題,當時只能大方的認可不會。痛定思痛,回去好好的研究一番。 咱們將從JS的內存機制以及事件機制大量的🌰(例子)來了解棧、堆到底是個什麼玩意。概念比較多,不用死讀,全部的🌰內心想一遍,瀏覽器console看一遍就很清楚了。 let's gohtml

JS內存機制

由於JavaScript具備自動垃圾回收機制,因此對於前端開發來講,內存空間並非一個常常被說起的概念,很容易被你們忽視。特別是不少不專業的朋友在進入到前端以後,會對內存空間的認知比較模糊。前端

在JS中,每個數據都須要一個內存空間。內存空間又被分爲兩種,棧內存(stack)堆內存(heap)html5

棧內存通常儲存基礎數據類型

Number String Null Undefined Boolean 
 (es6新引入了一種數據類型,Symbol)
複製代碼

最簡單的🌰

var a = 1 
複製代碼

咱們定義一個變量a,系統自動分配存儲空間。咱們能夠直接操做保存在棧內存空間的值,所以基礎數據類型都是按值訪問。git

數據在棧內存中的存儲與使用方式相似於數據結構中的堆棧數據結構,遵循後進先出的原則。es6

堆內存通常儲存引用數據類型

堆內存的🌰

var b = { xi : 20 }
複製代碼

與其餘語言不一樣,JS的引用數據類型,好比數組Array,它們值的大小是不固定的。引用數據類型的值是保存在堆內存中的對象。JavaScript不容許直接訪問堆內存中的位置,所以咱們不能直接操做對象的堆內存空間。看一下下面的圖,加深理解。github

比較

面試

wechatimg104

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] 做爲對象存在於堆內存中
複製代碼

所以當咱們要訪問堆內存中的引用數據類型時,實際上咱們首先是從棧中獲取了該對象的地址引用(或者地址指針),而後再從堆內存中取得咱們須要的數據。ajax

測試

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裏打一遍,再結合下面的圖例,就很好理解了數組

wechatimg106

15282536739797

內存機制咱們瞭解了,又引出一個新的問題,棧裏只能存基礎數據類型嗎,咱們常常用的function存在哪裏呢?promise

瀏覽器的事件機制

一個常常被搬上面試題的🌰

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

wechatimg105

對象放在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

圖解

wechatimg107

執行棧裏面最早放的是全局做用域(代碼執行有一個全局文本的環境),而後再放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');
複製代碼

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’以前是由於全部微任務總會在下一個宏任務以前所有執行完畢。

仍是🌰

<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 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')
            })
        })

    })
})
複製代碼

1 2 promise1 3 promise2

總結回顧

  • 棧:

    • 存儲基礎數據類型
    • 按值訪問
    • 存儲的值大小固定
    • 由系統自動分配內存空間
    • 空間小,運行效率高
    • 先進後出,後進先出
    • 棧中的DOM,ajax,setTimeout會依次進入到隊列中,當棧中代碼執行完畢後,再將隊列中的事件放到執行棧中依次執行。
    • 微任務和宏任務
  • 堆:

    • 存儲引用數據類型
    • 按引用訪問
    • 存儲的值大小不定,可動態調整
    • 主要用來存放對象
    • 空間大,可是運行效率相對較低
    • 無序存儲,可根據引用直接獲取

廣而告之

本文發佈於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章
相關標籤/搜索