什么是堆?什么是棧?他們之間有什么區別和聯系?


1.C/C++的內存分配(與操作系統相關)上來說,堆(heap),棧(stack)屬於內存空間的一段區域。

如圖:

一個程序在內存上由BSS段、data段、text段三個組成的。在沒有調入內存前,可執行程序分為代碼段、數據區和未初始化數據區三部分。

BSS段:(Block Started by Symbol)通常是指用來存放程序中未初始化的全局變量的一塊內存區域,屬於靜態內存分配。BSS段的內容並不存放在磁盤上的程序文件中。原因是內核在程序開始運行前將它們設置為0,需要存放在程序文件中的只有正文段和初始化數據段。text段和data段在編譯時已經分配了空間,而BSS段並不占用可執行文件的大小,它是由鏈接器來獲取內存的。

數據段:(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域,屬於靜態內存分配。總結為:初始化的全局變量和靜態變量在已初始化區域,未初始化的全局變量和靜態變量在BSS區。

代碼段:(code segment/text segment)通常是指用來存放程序執行代碼的一塊內存區域。該區域的大小在程序運行前就已經確定,並且內存區域通常屬於只讀, 某些架構也允許代碼段為可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。

堆(heap):用於動態分配內存,位於BSS和棧中間的地址區域,由程序員申請分配和釋放。堆是從低地址位向高地址位增長,采用鏈式存儲結構。頻繁的malloc/free造成內存空間的不連續,會產生碎片。(經常問如何解決內存碎片?)當申請堆空間時庫函數是按照一定的算法搜索可用的足夠大的空間,因此堆的效率比棧要低的多。注:與數據結構中的堆不是一個概念,但堆的分配方式類似於鏈表。

棧(stack): 由編譯器自動釋放,存放函數的參數值、局部變量等。每當一個函數被調用時,該函數的返回類型和一些調用的信息被存放到棧中,這個被調用的函數再為它的自動變量和臨時變量在棧上分配空間。每調用一個函數一個新的棧就會被使用。棧區是從高地址位向低地址位增長的,是一塊連續的內存區域,最大容量是由系統預先定義好的,申請的棧空間超過這個界限時會提示溢出。

實例程序:

2.區別:

1)管理方式棧由編譯器自動管理,無需人為控制。而堆釋放工作由程序員控制,容易產生內存泄漏(memory leak)。

2)空間大小在32位系統下,堆內存可以達到4G的空間(虛擬內存的大小,有面試官問過),從這個角度來看堆內存大小可以很大。但對於棧來說,一般都是有一定的空間大小的(在VC6默認的棧空間大小是1M,也有默認2M的)。可以重新設置,如圖:

3)碎片問題堆頻繁new/delete會造成內存空間的不連續,造成大量的碎片,使程序效率降低(重點是如何解決?如內存池、伙伴系統等)。對棧來說不會存在這個問題,因為棧是先進后出,不可能有一個內存塊從棧中間彈出。在該塊彈出之前,在它上面的(后進的棧內容)已經被彈出。

4)生長方向堆生長(擴展)方向是向上的,也就是向着內存地址增加的方向;棧生長(擴展)方向是向下的,是向着內存地址減小的方向增長, 可以看第一張圖。

5)分配方式堆都是動態分配的,沒有靜態分配的堆。而棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,如局部變量分配。動態分配由alloca函數進行分配,但是棧的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,無需我們手工實現。

6)效率棧是機器系統提供的數據結構,計算機會在底層對棧提供支持(有專門的寄存器存放棧的地址,壓棧出棧都有專門的機器指令執行),這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很復雜的(可以了解侯捷老師的內存管理的視頻,關於malloc/realloc/free函數等)。例如分配一塊內存,堆會按照一定的算法,在堆內存中搜索可用的足夠大小的空間,如果沒有(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然后進行返回。總之,堆的效率比棧要低得多。

以上,希望對面試有些幫助,菜雞 寫的難免有些不足,望大佬多多包涵~


【面試八股文】常見問題之 堆棧區別​www.nowcoder.com
 
圖標

個人感覺這里的堆 應該指的是heap而非數據結構中的堆。
棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。

區別和聯系:
1.申請方式
堆是由程序員自己申請並指明大小,在c中malloc函數 如p1 = (char *)malloc(10);
棧由系統自動分配,如聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間
2.申請后系統的響應
棧:只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會 遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內 存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大 小,系統會自動的將多余的那部分重新放入空閑鏈表中。
3.申請大小的限制
棧:在Windows下,棧是向低地址擴展的數據結 構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是 一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
4.申請效率的比較:
棧由系統自動分配,速度較快。但程序員是無法控制的。
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.

體會:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等准備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大。
 
--------------------------------------------------------------------------------
 

首先堆(heap)和棧(stack)兩個重名不是翻譯問題,而是英文原文就是一樣的。

 

數據結構中堆是滿足父子節點大小(比如大根堆中規定父節點的值要比子節點大)關系的一種完全二叉樹。由於是完全二叉樹,可以用數組來實現,用節點編號來訪問和操作節點,簡化程序,提升效率。而其大小關系則為我們查詢堆中極值提供了常數級別的時間復雜度,又由二叉樹的性質,插入和刪除則為對數級別時間復雜度。這就好像地位不同的人在排隊,排在最前面的一定是地位最高的人,所以堆是優先隊列(Priority Queue)實現的基礎。利用這一特性,可以加速某些需要頻繁取隊列中極值的算法比如 A* 算法等。

 

數據結構中的棧則是一種相當簡單的結構。就像是只有一個口的深深的文件桶,先進去的文件會被壓在下面(push),而且我們每次只能取到最上面的文件(pop),體現了其先進后出(FILO)的特性。雖然棧操作簡單,但也有如單調棧等在棧內保持一定數據特性的變種。

 

操作系統中的堆和棧都是指內存空間,不同的是堆為按需申請、動態分配,例如 C 中的 malloc 函數和 C++ 中的 new 操作(當然 C++ 的 new 不僅僅是申請內存這么簡單)。內存中的空閑空間並不是連續的,而是不同程序占用了不同的一塊一塊的內存,即使是同一個程序也可能占用了不同地方的多塊內存。操作系統中則會對這些空間進行統一的管理,在應用程序提出申請時,就會從堆中按照一定算法找出一塊可用內存,標記占用空間等信息之后返回其起始地址給程序。在程序結束之前,操作系統不會刪除已經申請的內存,而是要靠程序主動提出釋放的請求(free、delete),如果使用后忘記釋放,就會造成所謂的內存泄漏問題。因此堆基本上可以理解為當前可以使用的空閑內存,但是其申請和釋放都要程序員自己寫代碼管理。

 

而操作系統的棧則是程序運行時自動擁有的一小塊內存,大小在編譯期時由編譯器參數決定,用於局部變量的存放或者函數調用棧的保存。在 C 中如果聲明一個局部變量(例如 int a),它存放的地方就在棧中,而當這個局部變量離開其作用域之后,所占用的內存則會被自動釋放,因此在 C 中局部變量也叫自動變量。棧的另一個作用則是保存函數調用棧,這時和數據結構的棧就有關系了。在函數調用過程中,常常會多層甚至遞歸調用。每一個函數調用都有各自的局部變量值和返回值,每一次函數調用其實是先將當前函數的狀態壓棧,然后在棧頂開辟新空間用於保存新的函數狀態,接下來才是函數執行。當函數執行完畢之后,棧先進后出的特性使得后調用的函數先返回,這樣可以保證返回值的有序傳遞,也保證函數現場可以按順序恢復。操作系統的棧在內存中高地址向低地址增長,也即低地址為棧頂,高地址為棧底。這就導致了棧的空間有限制,一旦局部變量申請過多(例如開個超大數組),或者函數調用太深(例如遞歸太多次),那么就會導致棧溢出(Stack Overflow),操作系統這時候就會直接把你的程序殺掉。

 

--------------------------------------------------------------------------------

比較形象的比較是通過有道詞典的這兩張圖來做比較,然后展開聯想.

區別一(who):

1.堆:圖片為土堆,一堆土.堆上面的是程序員負責分配和負責釋放的.而程序員又稱作碼農,農民不就是挖土的嗎,活該不能智能的處理,只能農民(碼農)手動處理.

2.棧:圖片為書,讀書人,有知識有文化,什么都是自動的,高科技.所以棧是不用我們關心的,是操作系統自己去申請和釋放的.另一種記憶方法:棧是什么?客棧的棧,供旅客住宿的地方。計算機領域指數據暫時存儲的地方,所以才有進棧、出棧的說法。你去客棧還需要自己打掃嗎?不需要,所以對旅客來說不用自己去開辟,不用自己去申請和釋放。只要給錢就行了。

區別二(where):

1.堆:因為是土堆,農業相關的詞匯,農業就感覺比較傳統,沒有那么智能化,所以分配的時候會去查找合適的空間.另外,我們可以想象這樣一個場景:在懸崖邊的棧道(形似鏈表)上面放一堆土堆,那肯定是會全部漏下去的, 但是計算機相反,計算機內部偏偏不信邪,就要這么搞, 所以這里聯想到棧道來分配堆的.即:堆的分配會在棧道(鏈表)上去找,找到能放下堆的容量的內存塊的時候就分配給你.

2.棧:棧人家是書,是很智能化的,所以操作系統會直接給你了.

區別三(how):

1.堆:圖片是土堆,所以只能夠從地上網上堆,不可能相反,那樣的土堆除非有502.所以是從下往上分配地址的.

2.棧:圖為書,我們可以想像成一個未來的圖書館,圖書館都是把書放在天花板上面,從上往下放,而且不會掉下來.所以棧的分配是自頂向下的,而且肯定有空間限制.所以如果棧上空間不足了就overflow了.

 

區別四(result):

1.堆:圖片是土堆,農業相關,所以什么都比較慢,所以分配就比較慢了,而且到處都是小土堆,這就是碎片.

2.棧:圖片是書,高科技的,當然分配就很快,至少比堆塊.而且不會有碎片,人家是高科技嘛.

 
 
 
 
--------------------------------------------------------------------------------
 
 

這個問題, 長篇大論可以講很多,深入探討的話可以探討出很多問題,感興趣的,可以讀讀SICP這本書 Welcome to the SICP Web Site

 

我簡單列個矩陣,但只包含新手最關心,也是前期必須要理解的幾個方面,當然,也是脫離特定語言的。

 

 

 --------------------------------------------------------------------------------

 

我也產生過同樣疑問,我猜測數據結構的堆和棧與操作系統說的堆和棧應該是有聯系的,但是至今沒有去查歷史資料印證這個猜測。

關於棧就不多說了,我猜測先有了數據結構的“堆”,也許早期很原始,當時也許就是為了管理內存塊用的,所以,可以合理推測當時是在數據結構“堆”的指導下實現了操作系統的堆。

我們把“堆”這個概念限制到關於malloc相關的范圍,那么,在學習計算機系統課的時候,書上很明確說了歷史上出現過多種堆的實現,即使當前,不同操作系統的堆也是不一樣的。而且,為了特定目的可以實現自己需要的堆,在特定場景下更加有效的malloc,比如,用Priority Queue管理一堆預先划分好的確定大小的內存塊。而且在linux操作系統上,運行某些系統命令都可以加載自己做的一套malloc相關的管理函數。

那么,我們可以推想一下,以前一定有人用數據結構的堆實現了一個操作系統的堆,數據結構講的堆會很有效。分配內存的時候,要盡量快地找到一個剛好合適的塊;釋放內存以后,有些具體實現要完成碎片的合並,那么導致產生一塊更大的塊。這些用數據結構的堆來管理都很有效,可以實現快速的調整順序和快速的按照大小查找。

 

--------------------------------------------------------------------------------
 

在數據結構中經常有人說堆棧,堆棧之間有什么區別了?這些數據結構在算法思想中有哪些良好的運用了?

首先我們平常端盤子,都是知道盤子一般都是一個個堆疊起來的,而我們把盤子比作數據,而堆起來的盤子就很像棧這種數據結構

 

 

先進后出的這種結構,就是棧的數據結構,我們存數據時先存a,b,c,d,e 之后取e,d,c,b,a。

同樣的堆也是一類特殊的數據結構,和棧有所不同的是,堆的內存是由所在語言系統環境管理的(比如python虛擬機)而棧申請內存時是由系統自動分配的,而且系統分配也是大小限制的,超過這個大小后就會內存溢出報錯,類似如下定義的字符溢出。

 

 

堆內存是一般是由語言運行環境分配管理,如Python虛擬機控制內存和運行環境:

 

 

所以內存上可以組合關聯起來,這樣就不容易受系統限制

 

 

堆在數據存取上和棧有所不同,棧是先進后出,而堆是一種優先的隊列(並不是隊列,堆通常是一個可以被看做一棵樹的數組對象。),所謂優先隊列是按照元素的優先級取出元素。舉個例子,一般在飯桌上,無論你是先來后到,應該先是爺爺奶奶輩先動筷子,后面是父母,之后是你,像這種:

 

 

有這樣的優先級享受的,其嚴格的邏輯數據結構定義如下:

 

 

有了這定義我們來嘗試一下用堆這種數據結構做一種排序叫堆排序,我們先得到需要排序的一組元素:

16,7,3,20,17,8

先構建一顆樹:

 

 

按照堆數據結構的定義,即任何一非葉節點的數據不大於或者不小於其左右孩子節點的數據

,調整如下:

 

 

 

 

 

 

 

 

直到調整如上,就表示最大的在最上面,下面每個子節點都比父節點小,堆就完成了初始化。現在要對堆進行排序(注意),我們希望從左到右,按照從小到大的順序排列,依照堆的性質,我們可以這樣排:

首先我們將最大的和最后一個位置進行交換

 

 

除了紅色標記的元素,其他數據按照堆的性質從新排列好。

 

 

繼續將除紅色標記外的最大元素移到最后面

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

一直到只剩根節點時,這個時候排序就已經排好了,以上就是利用數據結構堆做的排序算法。我們用Python代碼實現如下:

def heap_sort(lst):

def sift_down(start, end):

"""最大堆調整"""

root = start

while True:

child = 2 * root + 1

if child > end:

break

if child + 1 <= end and lst[child] < lst[child + 1]:

child += 1

if lst[root] < lst[child]:

lst[root], lst[child] = lst[child], lst[root]

root = child

else:

break

# 創建最大堆

for start in range((len(lst) - 2) // 2, -1, -1):

sift_down(start, len(lst) - 1)

# 堆排序

for end in range(len(lst) - 1, 0, -1):

lst[0], lst[end] = lst[end], lst[0]

sift_down(0, end - 1)

return lst

if __name__ == "__main__":

list = [16,7,3,20,17,8]

heap_sort(list)

print(list)

運行結果:

 

 

計算復雜度后我們發現堆排序也是比較優秀的一種排序,那棧了,其實這里突然想起一件事,就是學維特比算法時有個馬爾科夫鏈,其中就包含了回溯算法,網上搜索發現棧這種數據結構正好對應這種算法思想,我們寫個實際的例子來看看,首先我們看看,什么是回溯了,我舉個例子。

如下圖,現在有一只小螞蟻P要在迷宮中找食物T,找的方式如下,我們把每個岔路口當成一個節點,記錄下來,假設螞蟻從節點1出發,沿着2、3、4都是死路,於是返回節點1,這個返回的過程就是回溯,當到達節點5時,往上碰壁返回來(回溯),左右碰壁回來(回溯),只有往下才找到食物,這個過程就是回溯算法體現。

 

 

為了方便代碼實現,我們用1表示牆,0表示可以走的地方,666表示食物,矩陣表示整個迷宮:

 

想法和思路是這樣:

1、將小螞蟻走過的分岔路口記錄下來

2、將小螞蟻走過的路徑記錄記錄下來(用棧這種數據結構)

3、當小螞蟻發現這條路不通時,或者遇到相同的岔路口時,延原路返回(路徑數據取出棧的數據)當回到岔路口時,將岔路口那個方向路不通的數據標記,延沒有標記的方向繼續前進(如果都標記了,繼續延原路返回),直到找到食物。

具體的實現代碼我就不貼出來了,這就是棧這種數據結構在回溯算法上的應用。

哈哈哈,折騰了一天希望有大佬能指出錯誤,共同進步。

參考書籍:

算法導論原書第三版

Python數據結構

原文來自:圖標

 

 

-----------------------------------------------------------------

首先說明這里的堆和棧不等同於數據結構中堆和棧的概念。這里的堆和棧都是內存中的一部分,有着不同的作用,而且一個程序需要在這片區域上分配內存。

1 棧內存(stack)

棧是用來存儲函數內部(包括main函數)的局部變量和方法調用和函數參數值,是由系統自動分配的,一般速度較快;存儲地址是連續且存在有限棧容量,會出現溢出現象。

2 堆內存(heap)

堆是由corder手動分配釋放的,通過malloc和new等動態申請內存的語句使用,也需要用戶手動回收(或可能在程序結束時OS自動回收),而對於面向對象程序來說,new出來的任何對象,無論是對象內部的成員變量,局部變量,類變量,他們指向的對象都存儲在堆內存中(但指針本身存在棧中),一般速度較棧慢;存儲地址通常是鏈式的,內存較大不會溢出。


個人的理解,有錯希望大牛指出,謝謝。

-----------------------------------------------------------------

 

前言

其實一開始對棧、堆的概念特別模糊,只知道好像跟內存有關,又好像事件循環也沾一點邊。面試薄荷的時候,面試官正好也問到了這個問題,當時只能大方的承認不會。痛定思痛,回去好好的研究一番。 我們將從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 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里打一遍,再結合下面的圖例,就很好理解了

內存機制我們了解了,又引出一個新的問題,棧里只能存基礎數據類型嗎,我們經常用的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(

概念:微任務和宏任務都是屬於隊列,而不是放在棧中

一個新的


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

 

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

-----------------------------------------------------------------

堆:堆中一般用來存放引用數據類型,索引一般存放在堆中

堆中內存大,所以存放引用數據類型

棧:棧中一般用來存放基本數據類型,棧中遵循一個原則,先進后出(最先進來的最后出去)

為什么是用來存放基本數據類型: 因為棧中 速度快,內存小

 

-----------------------------------------------------------------

堆 先進先出 排隊吃飯
棧 先進后出 子彈夾
 
 
 
棧是計算機管理,堆是程序員管理~
棧不可隨機存取,堆可以~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM