'「譯」理解Javascript函數執行—調用棧、事件循環、任務等'

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

JS Stack Visualization

當我們運行上圖中的代碼,我們會先尋找所有執行的開端——主函數。在上例中,一系列執行開始於console.log(bar(6)),那麼這一次執行就被推入調用棧中,它上面一層就是函數bar及其參數,函數bar轉而調用函數foo,foo也被推入棧中;而foo隨即return了某個值,所以被彈出調用棧;類似地,bar隨後彈出,最後console語句打印了結果並彈出。所有這些舉動都依次發生在須臾之間。

你們肯定都在瀏覽器控制檯見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式,簡單表明了調用棧的當前狀態以及在函數中何處報錯(見下圖)。

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

JS Stack Visualization

當我們運行上圖中的代碼,我們會先尋找所有執行的開端——主函數。在上例中,一系列執行開始於console.log(bar(6)),那麼這一次執行就被推入調用棧中,它上面一層就是函數bar及其參數,函數bar轉而調用函數foo,foo也被推入棧中;而foo隨即return了某個值,所以被彈出調用棧;類似地,bar隨後彈出,最後console語句打印了結果並彈出。所有這些舉動都依次發生在須臾之間。

你們肯定都在瀏覽器控制檯見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式,簡單表明了調用棧的當前狀態以及在函數中何處報錯(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Error stack trace

有時候,當我們以遞歸的形式多次調用一個函數,就會陷入無限循環中,而對於Chrome瀏覽器來說,它對調用棧的大小的限制是16000層,超出限制就會終止程序並拋出達到棧上限錯誤(見下圖)。

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

JS Stack Visualization

當我們運行上圖中的代碼,我們會先尋找所有執行的開端——主函數。在上例中,一系列執行開始於console.log(bar(6)),那麼這一次執行就被推入調用棧中,它上面一層就是函數bar及其參數,函數bar轉而調用函數foo,foo也被推入棧中;而foo隨即return了某個值,所以被彈出調用棧;類似地,bar隨後彈出,最後console語句打印了結果並彈出。所有這些舉動都依次發生在須臾之間。

你們肯定都在瀏覽器控制檯見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式,簡單表明了調用棧的當前狀態以及在函數中何處報錯(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Error stack trace

有時候,當我們以遞歸的形式多次調用一個函數,就會陷入無限循環中,而對於Chrome瀏覽器來說,它對調用棧的大小的限制是16000層,超出限制就會終止程序並拋出達到棧上限錯誤(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

  1. :對象會被分配到堆——內存中的鬆散結構。所有的針對變量和對象的內存分配都在堆中進行。
  2. 隊列:一種Javascript運行時,包含了一個消息隊列,這個隊列就是一系列將被處理的信息和要執行的相關回調函數。當調用棧有足夠空間,就從隊列中取出一條消息並進行處理,該消息調用相關聯的函數(並因此產生一個初始化棧層)。當棧再次清空時,消息處理也就結束了。簡單說,這些消息被排成隊列,指定回調函數來響應外部異步事件(例如鼠標點擊或HTTP請求的響應)。諸如用戶點擊按鈕而沒有相應回調函數的情況,就不會有消息放入隊列中。

事件循環(event loop)

當我們評估JS代碼的性能時,要知道調用棧中的函數會讓程序或快或慢,console.log()會很快,但用for或while迭代成千上萬次就會慢一些,並且讓調用棧一直被佔用被阻塞著。這就叫做阻塞腳本,你可能在Webpage Speed Insights中見過。

網絡請求會慢,圖片請求會慢,但萬幸,服務請求可以通過AJAX這種異步函數完成。假如那些網絡請求用同步函數來完成,將會如何?網絡請求發送到服務器——服務器也就是某處的某種機器罷了,現在假設服務器返回響應可能會緩慢,此時,如果我點擊一些CTA(call-to-action)按鈕,或者其他一些需要完成的渲染,就不會有什麼反應,因為調用棧還被之前的網絡請求阻塞著。在Ruby等多線程語言中,這種情況可以控制,但像Javascript這種單線程語言,除非調用棧中的函數返回值,否則就一直堵著。瀏覽器沒有任何反應,網頁就會崩潰。這樣我們可沒辦法為最終用戶提供流暢的用戶界面。那我們怎麼辦?

“JS中的併發——一次只做一件事,異步回調除外”

最早的解決方案就是用異步回調,這意味著我們給某部分代碼加一個回調,該回調會在這段代碼執行完成後執行。我們肯定都遇到過諸如AJAX請求用的$.get()、setTimeout()、setInterval()、Promises的異步回調。Node都是基於異步函數執行的。所有那些異步回調不會像console.log()等同步函數那樣立刻運行,而是在之後的某個時刻運行,所以不會立刻就推到調用棧中去。那它們到底去哪裡了?怎麼控制它們?

"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

JS Stack Visualization

當我們運行上圖中的代碼,我們會先尋找所有執行的開端——主函數。在上例中,一系列執行開始於console.log(bar(6)),那麼這一次執行就被推入調用棧中,它上面一層就是函數bar及其參數,函數bar轉而調用函數foo,foo也被推入棧中;而foo隨即return了某個值,所以被彈出調用棧;類似地,bar隨後彈出,最後console語句打印了結果並彈出。所有這些舉動都依次發生在須臾之間。

你們肯定都在瀏覽器控制檯見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式,簡單表明了調用棧的當前狀態以及在函數中何處報錯(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Error stack trace

有時候,當我們以遞歸的形式多次調用一個函數,就會陷入無限循環中,而對於Chrome瀏覽器來說,它對調用棧的大小的限制是16000層,超出限制就會終止程序並拋出達到棧上限錯誤(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

  1. :對象會被分配到堆——內存中的鬆散結構。所有的針對變量和對象的內存分配都在堆中進行。
  2. 隊列:一種Javascript運行時,包含了一個消息隊列,這個隊列就是一系列將被處理的信息和要執行的相關回調函數。當調用棧有足夠空間,就從隊列中取出一條消息並進行處理,該消息調用相關聯的函數(並因此產生一個初始化棧層)。當棧再次清空時,消息處理也就結束了。簡單說,這些消息被排成隊列,指定回調函數來響應外部異步事件(例如鼠標點擊或HTTP請求的響應)。諸如用戶點擊按鈕而沒有相應回調函數的情況,就不會有消息放入隊列中。

事件循環(event loop)

當我們評估JS代碼的性能時,要知道調用棧中的函數會讓程序或快或慢,console.log()會很快,但用for或while迭代成千上萬次就會慢一些,並且讓調用棧一直被佔用被阻塞著。這就叫做阻塞腳本,你可能在Webpage Speed Insights中見過。

網絡請求會慢,圖片請求會慢,但萬幸,服務請求可以通過AJAX這種異步函數完成。假如那些網絡請求用同步函數來完成,將會如何?網絡請求發送到服務器——服務器也就是某處的某種機器罷了,現在假設服務器返回響應可能會緩慢,此時,如果我點擊一些CTA(call-to-action)按鈕,或者其他一些需要完成的渲染,就不會有什麼反應,因為調用棧還被之前的網絡請求阻塞著。在Ruby等多線程語言中,這種情況可以控制,但像Javascript這種單線程語言,除非調用棧中的函數返回值,否則就一直堵著。瀏覽器沒有任何反應,網頁就會崩潰。這樣我們可沒辦法為最終用戶提供流暢的用戶界面。那我們怎麼辦?

“JS中的併發——一次只做一件事,異步回調除外”

最早的解決方案就是用異步回調,這意味著我們給某部分代碼加一個回調,該回調會在這段代碼執行完成後執行。我們肯定都遇到過諸如AJAX請求用的$.get()、setTimeout()、setInterval()、Promises的異步回調。Node都是基於異步函數執行的。所有那些異步回調不會像console.log()等同步函數那樣立刻運行,而是在之後的某個時刻運行,所以不會立刻就推到調用棧中去。那它們到底去哪裡了?怎麼控制它們?

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

如上例,若一個網絡請求在Javascript中運行:

1. 請求函數被執行,給`onreadystatechange`事件傳一個匿名函數作為回調,用來在將來響應就緒的時候執行。
2. “Script call done!”立刻輸出到控制檯。
3. 後續某時刻,響應被返回,回調被執行,響應體被輸出到控制檯。

在等待異步操作完成並解除回調執行之時,響應的解耦調用允許Javascript運行時做別的事。瀏覽器插入進來調用了它的API,這是用C++實現的API,用來創建線程以控制諸如DOM事件、http請求、setTimeout等異步事件。

那些web接口不能自己把執行代碼推入調用棧,如果能,那麼該接口會隨機出現在你的代碼中(執行順序不可控)。上面討論過的消息回調隊列說明了這一點。任何web接口在執行完畢後,都會把回調推入這個隊列。事件循環此時就要負責控制隊列中的回調的執行,並在棧空時把回調推入棧中。事件循環的基本工作就是監聽調用棧和任務隊列,當它看到棧空了,就把隊列中第一個任務推入棧。每個消息或者回調都在上一個任務處理完再開始處理。

while (queue.waitForMessage()) {
queue.processNextMessage();
}
"

現如今,web開發者(我們更喜歡被叫做前端工程師)用一門腳本語言就能做任何事情,從提供瀏覽器中的交互,到開發電腦遊戲、桌面工具、跨平臺移動應用,甚至可以在服務端部署(如最流行的Node.js)來連結任意數據庫。因此,瞭解Javascript的內部構造很重要,這樣才能更優更高效的使用它。這也是本文的主旨所在。

Javascript的生態正在變得越來越複雜。要構建一個現代web應用,會不可避免的用到Webpack、Babel、ESLint、Mocha、Karma、Grunt……我該用哪個?這些都是幹嘛的?我找到了這個漫畫,它完美詮釋瞭如今的web開發者的水深火熱:

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript疲勞症——學習Javascript是什麼感覺

在一頭扎進框架和庫的海洋之前,每個Javascript開發者首先需要了解Javascript在底層是如何實現的。差不多每個JS開發者都聽過“V8”這個術語,但有些人可能根本不知道這個詞到底什麼意思、幹嘛用的。在我職業開發生涯的第一年裡,我對這些花裡胡哨的術語所知甚少,我更關心先完成工作。但這樣並不能滿足我的好奇心,我好奇Javascript是他喵的怎麼能做到這一切的。我決定要深挖一番,我翻遍Google,找到一些優秀的博客,包括Philip Roberts的a great talk at JSConf on the event loop。所以我決定總結我的學習經驗並分享出來。鑑於有太多東西要了解,我把本文分為兩個部分。這一部分會介紹常用術語,第二部分則會闡述這些術語之間的關聯。

Javascript是一個單線程單併發的語言,也就是說它一次只能處理一個任務,執行一條代碼。它的調用棧連同堆、隊列一起構成了Javascript併發模型(在V8中實現)。讓我們一個個地看這幾個詞。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Visual Representation of JS Model

  1. 調用棧(Call Stack):它是記錄我們在程序中調用函數的數據結構。假如我們調用一個函數來執行,就是在把某種記錄推入到調用棧的頂端;當我們從一個函數中返回出來,就從調用棧頂端彈出記錄。
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

JS Stack Visualization

當我們運行上圖中的代碼,我們會先尋找所有執行的開端——主函數。在上例中,一系列執行開始於console.log(bar(6)),那麼這一次執行就被推入調用棧中,它上面一層就是函數bar及其參數,函數bar轉而調用函數foo,foo也被推入棧中;而foo隨即return了某個值,所以被彈出調用棧;類似地,bar隨後彈出,最後console語句打印了結果並彈出。所有這些舉動都依次發生在須臾之間。

你們肯定都在瀏覽器控制檯見過那個又長又紅的報錯棧,它用一種從上到下的恰如棧的方式,簡單表明了調用棧的當前狀態以及在函數中何處報錯(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Error stack trace

有時候,當我們以遞歸的形式多次調用一個函數,就會陷入無限循環中,而對於Chrome瀏覽器來說,它對調用棧的大小的限制是16000層,超出限制就會終止程序並拋出達到棧上限錯誤(見下圖)。

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

  1. :對象會被分配到堆——內存中的鬆散結構。所有的針對變量和對象的內存分配都在堆中進行。
  2. 隊列:一種Javascript運行時,包含了一個消息隊列,這個隊列就是一系列將被處理的信息和要執行的相關回調函數。當調用棧有足夠空間,就從隊列中取出一條消息並進行處理,該消息調用相關聯的函數(並因此產生一個初始化棧層)。當棧再次清空時,消息處理也就結束了。簡單說,這些消息被排成隊列,指定回調函數來響應外部異步事件(例如鼠標點擊或HTTP請求的響應)。諸如用戶點擊按鈕而沒有相應回調函數的情況,就不會有消息放入隊列中。

事件循環(event loop)

當我們評估JS代碼的性能時,要知道調用棧中的函數會讓程序或快或慢,console.log()會很快,但用for或while迭代成千上萬次就會慢一些,並且讓調用棧一直被佔用被阻塞著。這就叫做阻塞腳本,你可能在Webpage Speed Insights中見過。

網絡請求會慢,圖片請求會慢,但萬幸,服務請求可以通過AJAX這種異步函數完成。假如那些網絡請求用同步函數來完成,將會如何?網絡請求發送到服務器——服務器也就是某處的某種機器罷了,現在假設服務器返回響應可能會緩慢,此時,如果我點擊一些CTA(call-to-action)按鈕,或者其他一些需要完成的渲染,就不會有什麼反應,因為調用棧還被之前的網絡請求阻塞著。在Ruby等多線程語言中,這種情況可以控制,但像Javascript這種單線程語言,除非調用棧中的函數返回值,否則就一直堵著。瀏覽器沒有任何反應,網頁就會崩潰。這樣我們可沒辦法為最終用戶提供流暢的用戶界面。那我們怎麼辦?

“JS中的併發——一次只做一件事,異步回調除外”

最早的解決方案就是用異步回調,這意味著我們給某部分代碼加一個回調,該回調會在這段代碼執行完成後執行。我們肯定都遇到過諸如AJAX請求用的$.get()、setTimeout()、setInterval()、Promises的異步回調。Node都是基於異步函數執行的。所有那些異步回調不會像console.log()等同步函數那樣立刻運行,而是在之後的某個時刻運行,所以不會立刻就推到調用棧中去。那它們到底去哪裡了?怎麼控制它們?

「譯」理解Javascript函數執行—調用棧、事件循環、任務等

如上例,若一個網絡請求在Javascript中運行:

1. 請求函數被執行,給`onreadystatechange`事件傳一個匿名函數作為回調,用來在將來響應就緒的時候執行。
2. “Script call done!”立刻輸出到控制檯。
3. 後續某時刻,響應被返回,回調被執行,響應體被輸出到控制檯。

在等待異步操作完成並解除回調執行之時,響應的解耦調用允許Javascript運行時做別的事。瀏覽器插入進來調用了它的API,這是用C++實現的API,用來創建線程以控制諸如DOM事件、http請求、setTimeout等異步事件。

那些web接口不能自己把執行代碼推入調用棧,如果能,那麼該接口會隨機出現在你的代碼中(執行順序不可控)。上面討論過的消息回調隊列說明了這一點。任何web接口在執行完畢後,都會把回調推入這個隊列。事件循環此時就要負責控制隊列中的回調的執行,並在棧空時把回調推入棧中。事件循環的基本工作就是監聽調用棧和任務隊列,當它看到棧空了,就把隊列中第一個任務推入棧。每個消息或者回調都在上一個任務處理完再開始處理。

while (queue.waitForMessage()) {
queue.processNextMessage();
}
「譯」理解Javascript函數執行—調用棧、事件循環、任務等

Javascript Event Loop Visual Representation

在web瀏覽器中,一旦某事件發生並綁定了事件監聽器,消息就立即添加到隊列中。如果沒有監聽器,那就意味著事件丟失了。因此點擊一個綁定了點擊事件處理器,就會新增一個消息,其他事件亦如此。對其回調的調用將會是調用棧中的初始層,而由於Javascript是單線程的,在調用棧中所有調用都return之前,後續的消息的輪詢和處理就暫停了。之後的(同步的)函數調用會向調用棧中增加新的調用層。

在下一部分,我會通過一個動畫來展示上述過程的代碼執行,深入解釋什麼是不同類型的異步函數、隊列中誰優先執行,以及諸如零延遲等功能的技巧。

"

相關推薦

推薦中...