前端進階算法5:全方位解讀前端用到的棧結構(調用棧、堆、垃圾回收等)

引言

棧結構很簡單,咱們能夠經過數組就能模擬出一個棧結構,但僅僅介紹棧結構就太不前端了,本節從棧結構開始延伸到瀏覽器中 JavaScript 運行機制,還有存儲機制上用到的棧結構及相關數據結構,一文吃透全部的前端棧知識。前端

之後再提到棧時,咱們再也不僅限於 LIFO 了,而是一個有深度的棧。git

這部分是前端進階資深必備,若是你想打造高性能的前端應用,也須要了解這塊,同時它也是面試的常見考察點。github

理解棧對於咱們理解 JavaScript 語言相當重要,本文主要從如下幾個方面介紹棧:面試

  • 首先介紹棧及代碼實現
  • 介紹 JavaScript 運行機制及棧在其中的應用
  • 詳細介紹調用棧及咱們開發中如何利用調用棧
  • JS 內存機制:棧(基本類型、引用類型地址)與堆(引用類型數據)
  • 最後來一份總結與字節&leetcode刷題,實現最小棧

本節吃透棧原理,以後幾天會每日一題,刷透棧題目,下面進入正文吧👍算法

1、 棧

棧是一種聽從後進先出 (LIFO / Last In First Out) 原則的有序集合,它的結構相似以下:數組

棧的操做主要有: push(e) (進棧)、 pop() (出棧)、 isEmpty() (判斷是不是空棧)、 size() (棧大小),以及 clear() 清空棧,具體實現也很簡單。瀏覽器

2、代碼實現

function Stack() {
  let items = []
  this.push = function(e) { 
    items.push(e) 
  }
  this.pop = function() { 
    return items.pop() 
  }
  this.isEmpty = function() { 
    return items.length === 0 
  }
  this.size = function() { 
    return items.length 
  }
  this.clear = function() { 
    items = [] 
  }
}

查找:從棧頭開始查找,時間複雜度爲 O(n)數據結構

插入或刪除:進棧與出棧的時間複雜度爲 O(1)異步

3、瀏覽器中 JS 運行機制

咱們知道 JavaScript 是單線程的,所謂單線程,是指在 JavaScript 引擎中負責解釋和執行 JavaScript 代碼的線程惟一,同一時間上只能執行一件任務。函數

爲何是單線程的喃?這是由於 JavaScript 能夠修改 DOM 結構,若是 JavaScript 引擎線程不是單線程的,那麼能夠同時執行多段 JavaScript,若是這多段 JavaScript 都修改 DOM,那麼就會出現 DOM 衝突。

爲了不 DOM 渲染的衝突,能夠採用單線程或者死鎖,JavaScript 採用了單線程方案。

但單線程有一個問題:若是任務隊列裏有一個任務耗時很長,致使這個任務後面的任務一直排隊等待,就會發生頁面卡死,嚴重影響用戶體驗。

爲了解決這個問題,JavaScript 將任務的執行模式分爲兩種:同步和異步。

同步

// 同步任務
let a = 1
console.log(a) // 1

異步

// 異步任務
setTimeout(() => {
    console.log('時間到')
}, 1000)

同步任務都在主線程(這裏的主線程就是 JavaScript 引擎線程)上執行,會造成一個 調用棧 ,又稱 執行棧

除了主線程外,還有一個任務隊列(也稱消息隊列),用於管理異步任務的 事件回調 ,在 調用棧 的任務執行完畢以後,系統會檢查任務隊列,看是否有能夠執行的異步任務。

注意:任務隊列存放的是異步任務的事件回調

例如上例:

setTimeout(() => {
    console.log('時間到')
}, 1000)

在執行這段代碼時,並不會馬上打印 ,只有定時結束後(1s)纔打印。 setTimeout 自己是同步執行的,放入任務隊列的是它的回調函數。

下面咱們重點看一下主線程上的調用棧。

4、調用棧

咱們從如下兩個方面介紹調用棧:

  • 調用棧的用來作什麼
  • 在開發中,如何利用調用棧

1. 調用棧的職責

咱們知道,在 JavaScript 中有不少函數,常常會出現一個函數調用另一個函數的狀況,調用棧就是用來管理函數調用關係的一種棧結構

那麼它是如何去管理函數調用關係喃?咱們舉例說明:

var a = 1
function add(a) {
  var b = 2
  let c = 3
  return a + b + c
}

// 函數調用
add(a)

這段代碼很簡單,就是建立了一個 add 函數,而後調用了它。

下面咱們就一步步的介紹整個函數調用執行的過程。

在執行這段代碼以前,JavaScript 引擎會先建立一個全局執行上下文,包含全部已聲明的函數與變量:

1584626593458-11e5d674-2ace-4209-a2d3-9a4484979a05.png

從圖中能夠看出,代碼中的全局變量 a 及函數 add 保存在變量環境中。

執行上下文準備好後,開始執行全局代碼,首先執行 a = 1 的賦值操做,

1584626811363-aa3a9f6b-5abd-4100-87e2-659847d04500.png

賦值完成後 a 的值由 undefined 變爲 1,而後執行 add 函數,JavaScript 判斷出這是一個函數調用,而後執行如下操做:

  • 首先,從全局執行上下文中,取出 add 函數代碼
  • 其次,對 add 函數的這段代碼進行編譯,並建立該函數的執行上下文和可執行代碼,並將執行上下文壓入棧中

1584628991777-0edc0564-b07f-46b4-9590-d038e948bb69.png

  • 而後,執行代碼,返回結果,並將 add 的執行上下文也會從棧頂部彈出,此時調用棧中就只剩下全局上下文了。

1584626811363-aa3a9f6b-5abd-4100-87e2-659847d04500.png

至此,整個函數調用執行結束了。

因此說,調用棧是 JavaScript 用來管理函數執行上下文的一種數據結構,它記錄了當前函數執行的位置,哪一個函數正在被執行。 若是咱們執行一個函數,就會爲函數建立執行上下文並放入棧頂。 若是咱們從函數返回,就將它的執行上下文從棧頂彈出。 也能夠說調用棧是用來管理這種執行上下文的棧,或稱執行上下文棧(執行棧)

2. 懂調用棧的開發人員有哪些優點

棧溢出

在咱們執行 JavaScript 代碼的時候,有時會出現棧溢出的狀況:

1584543285401-25e8a004-f729-44a0-9ac6-70e0a564285d.png

上圖就是一個典型的棧溢出,那爲何會出現這種錯誤喃?

咱們知道調用棧是用來管理執行上下文的一種數據結構,它是有大小的,當入棧的上下文過多的時候,它就會報棧溢出,例如:

function add() {
  return 1 + add()
}

add()

add 函數不斷的遞歸,不斷的入棧,調用棧的容量有限,它就溢出了,因此,咱們平常的開發中,必定要注意此類代碼的出現。

在瀏覽器中獲取調用棧信息

兩種方式,一種是斷點調試,這種很簡單,咱們平常開發中都用過。

一種是 console.trace()

function sum(){
  return add()
}
function add() {
  console.trace()
  return 1
}

// 函數調用
sum()

1584629886522-8d65269b-680b-48d7-ba32-b49e0531ad5d.png

5、JS 內存機制:棧(基本類型、引言類型地址)與堆(引用類型數據)

在 JavaScript 開發平常中,前端人員不多有機會了解內存,但若是你想成爲前端的專家,打造高性能的前端應用,你就須要瞭解這一塊,同時它也是面試的常見考察點。

JavaScript 中的內存空間主要分爲三種類型:

  • 代碼空間:主要用來存放可執行代碼
  • 棧空間:調用棧的存儲空間就是棧空間。
  • 堆空間

代碼空間主要用來存放可執行代碼的。棧空間及堆空間主要用來存放數據的。接下來咱們主要介紹棧空間及堆空間。

JavaScript 中的變量類型有 8 種,可分爲兩種:基本類型、引用類型

基本類型:

  • undefined
  • null
  • boolean
  • number
  • string
  • bigint
  • symbol

引用類型:

  • object

其中,基本類型是保存在棧內存中的簡單數據段,而引用類型保存在堆內存中。

1. 棧空間

基本類型在內存中佔有固定大小的空間,因此它們的值保存在棧空間,咱們經過 按值訪問

通常棧空間不會很大。

2. 堆空間

引用類型,值大小不固定,但指向值的指針大小(內存地址)是固定的,因此把對象放入堆中,將對象的地址放入棧中,這樣,在調用棧中切換上下文時,只須要將指針下移到上個執行上下文的地址就能夠了,同時保證了棧空間不會很大。

當查詢引用類型的變量時, 先從棧中讀取內存地址, 而後再經過地址找到堆中的值。對於這種,咱們把它叫作 按引用訪問

通常堆內存空間很大,能存放不少數據,但它內存分配與回收都須要花費必定的時間。

舉個例子幫助理解一下:

var a = 1
function foo() {
  var b = 2
  var c = { name: 'an' }
}

// 函數調用
foo()

基本類型(棧空間)與引用類型(堆空間)的存儲方式決定了:基本類型賦值是值賦值,而引用類型賦值是地址賦值。

// 值賦值
var a = 1
var b = a
a = 2
console.log(b) 
// 1
// b 不變

// 地址賦值
var a1 = {name: 'an'}
var b1 = a1
a1.name = 'bottle'
console.log(b1)
// {name: "bottle"}
// b1 值改變

3. 垃圾回收

JavaScript 中的垃圾數據都是由垃圾回收器自動回收的,不須要手動釋放。因此大部分的開發人員並不瞭解垃圾回收,但這部分也是前端進階資深必備!

回收棧空間

在 JavaScript 執行代碼時,主線程上會存在 ESP 指針,用來指向調用棧中當前正在執行的上下文,以下圖,當前正在執行 foo 函數:

foo 函數執行完成後,ESP 向下指向全局執行上下文,此時須要銷燬 foo 函數。

怎麼銷燬喃?

當 ESP 指針指向全局執行上下文,foo 函數執行上下文已是無效的了,當有新的執行上下文進來時,能夠直接覆蓋這塊內存空間。

即:JavaScript 引擎經過向下移動 ESP 指針來銷燬存放在棧空間中的執行上下文。

回收堆空間

V8 中把堆分紅新生代與老生代兩個區域:

  • 新生代:用來存放生存週期較短的小對象,通常只支持1~8M的容量
  • 老生代:用來存放生存週期較長的對象或大對象

V8 對這兩塊使用了不一樣的回收器:

  • 新生代使用副垃圾回收器
  • 老生代使用主垃圾回收器

其實不管哪一種垃圾回收器,都採用了一樣的流程(三步走):

  • 標記: 標記堆空間中的活動對象(正在使用)與非活動對象(可回收)
  • 垃圾清理: 回收非活動對象所佔用的內存空間
  • 內存整理: 當進行頻繁的垃圾回收時,內存中可能存在大量不連續的內存碎片,當須要分配一個須要佔用較大連續內存空間的對象時,可能存在內存不足的現象,因此,這時就須要整理這些內存碎片。

副垃圾回收器與主垃圾回收器雖然都採用一樣的流程,但使用的回收策略與算法是不一樣的。

副垃圾回收器

它採用 Scavenge 算法及對象晉升策略來進行垃圾回收

所謂 Scavenge 算法,即把新生代空間對半劃分爲兩個區域,一半是對象區域,一半是空閒區域,以下圖所示:

新加入的對象都加入對象區域,當對象區滿的時候,就執行一次垃圾回收,執行流程以下:

  • 標記:首先要對區域內的對象進行標記(活動對象、非活動對象)
  • 垃圾清理:而後進行垃圾清理:將對象區的活動對象複製到空閒區域,並進行有序的排列,當複製完成後,對象區域與空閒區域進行翻轉,空閒區域晉升爲對象區域,對象區域爲空閒區域

翻轉後,對象區域是沒有碎片的,此時不須要進行第三步(內存整理了)

但,新生代區域很小的,通常1~8M的容量,因此它很容易滿,因此,JavaScript 引擎採用對象晉升策略來處理,即只要對象通過兩次垃圾回收以後依然繼續存活,就會被晉升到老生代區域中。

主垃圾回收器

老生代區域裏除了存在重新生代晉升來的存活時間久的對象,當遇到大對象時,大對象也會直接分配到老生代。

因此主垃圾回收器主要保存存活久的或佔用空間大的對象,此時採用 Scavenge 算法就不合適了。V8 中主垃圾回收器主要採用標記-清除法進行垃圾回收。

主要流程以下:

  • 標記:遍歷調用棧,看老生代區域堆中的對象是否被引用,被引用的對象標記爲活動對象,沒有被引用的對象(待清理)標記爲垃圾數據。
  • 垃圾清理:將全部垃圾數據清理掉
  • 內存整理:標記-整理策略,將活動對象整理到一塊兒

增量標記

V8 瀏覽器會自動執行垃圾回收,但因爲 JavaScript 也是運行在主線程上的,一旦執行垃圾回收,就要打斷 JavaScript 的運行,可能會或多或少的形成頁面的卡頓,影響用戶體驗,因此 V8 決定採用增量 標記算法回收:

即把垃圾回收拆成一個個小任務,穿插在 JavaScript 中執行。

6、總結

本節從棧結構開始介紹,知足後進先出 (LIFO) 原則的有序集合,而後經過數組實現了一個棧。

接着介紹瀏覽器環境下 JavaScript 的異步執行機制,即事件循環機制, JavaScript 主線程不斷的循環往復的從任務隊列中讀取任務(異步事件回調),放入調用棧中執行。調用棧又稱執行上下文棧(執行棧),是用來管理函數執行上下文的棧結構。

JavaScript 的存儲機制分爲代碼空間、棧空間以及堆空間,代碼空間用於存放可執行代碼,棧空間用於存放基本類型數據和引用類型地址,堆空間用於存放引用類型數據,當調用棧中執行完成一個執行上下文時,須要進行垃圾回收該上下文以及相關數據空間,存放在棧空間上的數據經過 ESP 指針來回收,存放在堆空間的數據經過副垃圾回收器(新生代)與主垃圾回收器(老生代)來回收。

聊聊就跑遠了🤦‍♀️,但都是前端進階必會,接下來咱們開始刷棧題目吧!!!每日一刷,進階前端與算法⛽️⛽️⛽️,來道簡單的吧!

7、字節&leetcode155:最小棧(包含getMin函數的棧)

設計一個支持 pushpoptop 操做,並能在常數時間內檢索到最小元素的棧。

  • push(x) —— 將元素 x 推入棧中。
  • pop() —— 刪除棧頂的元素。
  • top() —— 獲取棧頂元素。
  • getMin() —— 檢索棧中的最小元素。

示例:

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.getMin();   --> 返回 -2.

歡迎將答案提交到 https://github.com/sisterAn/J...,讓更多人看到,瓶子君也會在明日放上本身的解答。

8、參考資料

瀏覽器工做原理與實踐(極客時間)

9、認識更多的前端道友,一塊兒進階前端開發

歡迎關注「前端瓶子君」,回覆「算法」自動加入,從0到1構建完整的數據結構與算法體系!

在這裏,瓶子君不只介紹算法,還將算法與前端各個領域進行結合,包括瀏覽器、HTTP、V八、React、Vue源碼等。

在這裏,你能夠天天學習一道大廠算法題(阿里、騰訊、百度、字節等等)或 leetcode,瓶子君都會在次日解答喲!

⬆️ 掃碼關注公衆號「前端瓶子君」,回覆「算法」便可自動加入 👍👍👍

相關文章
相關標籤/搜索