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

JS內存機制

因爲JavaScript具有自動垃圾回收機制,所以對於前端開發來說,內存空間並不是一個經常被提及的概念,很容易被大家忽視。特別是很多不專業的朋友在進入到前端之後,會對內存空間的認知比較模糊。

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

棧內存一般儲存基礎數據類型

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

最簡單的🌰

var a = 1 
複製代碼

我們定義一個變量a,系統自動分配存儲空間。我們可以直接操作保存在棧內存空間的值,因此基礎數據類型都是按值訪問。

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

堆內存一般儲存引用數據類型

堆內存的🌰

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

與其他語言不同,JS的引用數據類型,比如數組Array,它們值的大小是不固定的。引用數據類型的值是保存在堆內存中的對象。JavaScript不允許直接訪問堆內存中的位置,因此我們不能直接操作對象的堆內存空間。看一下下面的圖,加深理解。

比較

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

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

測試

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存在哪裏呢?

瀏覽器的事件機制

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

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會依次進入到隊列中,當棧中代碼執行完畢後,再將隊列中的事件放到執行棧中依次執行。
    • 微任務和宏任務
  • 堆:

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

轉載於:
作者:薄荷前端
鏈接:https://juejin.im/post/5b1deac06fb9a01e643e2a95
來源:掘金
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章