原文地址: How JavaScript works in browser and node?javascript
有很是多滿懷激情的開發者,他們搞前端或者搞後端,爲JavaScript奉獻本身青春和血汗。JavaScript是一種很是容易理解語言,毫無疑問它是前端開發中一個很是關鍵的部分。可是和其餘語言不一樣的是, 它是單線程的,這就意味着,同一時間只能有一個代碼片斷在執行。由於代碼執行是線性的,若是當中有任何代碼執行很長時間,將會阻塞後面須要被執行的代碼。所以有時候你會在Google Chrome中看到這樣的界面:前端
當你在瀏覽器打開一個網站時,它會使用一個JavaScript執行線程。這個線程負責響應一切操做,好比頁面滾動、頁面渲染、監聽DOM事件(好比用戶點擊按鈕)等等。可是若是JavaScript執行被阻塞了,那瀏覽器就什麼事情也作不了,即意味着瀏覽器會呈現爲卡死,沒法響應的現象。java
不信你就在控制檯輸入試試:node
while(true){}
// ...
複製代碼
你會上面語句以後的任何代碼都不會被執行,這個‘死循環’會霸佔着系統資源, 讓瀏覽器沒法響應用戶操做. 無限遞歸調用也會出現這種狀況, 不過下文會介紹,Javascript引擎對調用棧長度進行限制,無限遞歸會拋出RangeError異常, 而不會無休止地運行。git
感謝現代瀏覽器,如今不是全部打開的標籤頁都依賴於一個JavaScript線程。而是每一個標籤頁或者域名都會有獨立的JavaScript線程。這樣每一個標籤頁之間不會互相阻塞。好比你能夠在Chrome中打開多個標籤頁,在某個標籤頁下執行上面的死循環,你會發現只有執行了上面語句的標籤卡死,其餘不受影響。github
爲了可視化JavaScript 如何執行程序,咱們首先要理解JavaScript運行時。編程
和其餘編程語言同樣,JavaScript運行時有一個棧(Stack)和一個堆(Heap)存儲器。後端
上圖來源於[Fhinkel](Confused about Stack and Heap?)文章,關於棧和堆之間的差別講得比較清晰. 舉個例子:瀏覽器
在Java或者C#中, 值類型(primitives原始類型)存儲在棧中,而引用類型(reference)則存儲在堆中。C++規範沒有規定棧和堆的內存分配,而是使用自動存儲(automatic)期
和動態存儲(dynamic)期
來做區分,局部變量是自動存儲期,編譯器會將它們存儲在棧中。而動態分配的對象則一般保存在堆中。放在棧中的數據會在函數執行完畢後自動回收,而放在堆中的對象,若是沒有釋放就會形成內存泄露緩存
本文不會深刻解釋Heap,你能夠看這裏. 在本文咱們感興趣的是棧,棧是一個LIFO(後進先出)的數據結構,用來保存程序當前的函數執行上下文, 換句話說,它表示的是當前程序執行的位置. 每次開始執行一個函數,就會將該函數推入棧中,當函數返回時從棧中彈出。 當棧爲空時表示沒有程序正在執行。因此棧經常也稱爲‘調用棧’。
function baz(){
console.log('Hello from baz')
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
複製代碼
所以, 當上面的程序加載進內存時,會開始執行第一個函數,即foo
。 所以第一個棧元素就是foo()
, 由於foo
函數會調用bar
函數,第二個棧元素就是bar()
; 同理bar
函數會調用baz
,第三個棧元素就是baz()
. 最後,baz
調用console.log
,最後一個棧元素就是console.log('Hello from baz')
棧會在函數執行完畢時(到達函數底部或者調用return)彈出。而後繼續執行函數調用後續的語句:
每一個棧元素中,元素的狀態也被稱爲棧幀(Stack Frame). 若是在函數調用拋出錯誤,JavaScript會輸出棧跟蹤記錄(Stack trace),表示代碼執行時的棧幀的快照。
function baz(){
throw new Error('Something went wrong.');
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
複製代碼
上面的程序,咱們在baz
中拋出錯誤,JavaScript會打印出棧跟中記錄,指出錯誤發生的地方和錯誤信息。
棧的大小不是無限的。例如Chrome就會限定棧的最大爲16,000幀。因此無限遞歸會致使Chrome拋出Maximum Call Stack size exceeded
:
由於JavaScript是單線程的,因此它只有一個棧和堆。所以,若是其餘程序想要執行一些東西,須要等待上一個程序執行完畢
對比其餘語言,這多是一個糟糕的設計,可是JavaScript的定位就是通用編程語言,而不是用於很是複雜的場景
考慮這樣一個場景。假設瀏覽器發送一個HTTP請求到服務器,加載圖片並展現到頁面。瀏覽器會卡死等待請求完成嗎?顯然不會,這樣用戶體驗太差了
瀏覽器經過JavaScript引擎來提供JavaScript運行環境。好比Chrome使用V8 引擎。可是瀏覽器內部可不僅有JavaScript引擎。下面是瀏覽器的底層結構:
看起來很複雜,可是它也很好理解。JavaScript引擎須要和其餘2個組件協做,即事件循環(EventLoop)和回調隊列(CallbackQueue),回調隊列也被稱爲消息隊列或任務隊列。
除了JavaScript引擎,瀏覽器還包含了許多不一樣的應用來作各類各樣的事情,好比HTTP請求、DOM事件監聽、經過setTimeout、setInterval延遲執行、緩存、數據存儲等等。這些特性能夠幫助咱們建立豐富的Web應用。
想一下,若是瀏覽器只使用同一個JavaScript線程來處理上面這些特性,用戶體驗會有多糟糕。由於用戶即便只是簡單的滾動頁面,背後是須要處理不少事情的, 單個Javascript線程壓根忙不過來。所以瀏覽器會使用低級的語言,好比C++,來執行這些操做,並暴露簡潔的JavaScript API給開發者。這些API統稱爲Web API。
這些Web API一般是異步的。這意味着,你能夠命令這些API在'後臺'(獨立線程)去作一些事情,完成任務以後再通知Javascript運行時. 在此同時,Javascript引擎會繼續執行剩下的JavaScript代碼. 在命令這些API在後臺作事情時,咱們一般須要給它們提供一個回調。這個回調的職責就是在Web API完成任務後執行JavaScript代碼。讓咱們將上述的全部東西整合起來理解一下:
當你調用一個函數時,它會被推動棧中。若是這個函數中包含了Web API調用,JavaScript會代理Web API的調用, 通知Web API執行任務,接着繼續執行下一行代碼直到函數返回。一旦函數到達return語句或者函數底部,這個函數就會從調用棧中彈出來。
與此同時,若是Web API在後臺完成了它的工做,且有一個回調和這個工做綁定,Web API會將消息結果和回調進行綁定,並推入到消息隊列中(或者稱爲回調隊列).
事件循環, 就像一個無限循環,它的惟一工做是檢查回調隊列,一旦回調隊列中有待處理的任務,就將該回調推送到調用棧。不過由於Javascript是單線程的, 事件循環一次只能推送一個回調到調用棧,棧將會執行回調函數,一旦調用棧爲空,事件循環纔會將下一個回調函數推送到調用堆。
事件循環的僞代碼大概以下:
while(true) {
let task
while(task = popCallbackQueue()) {// 彈出回調隊列任務
executeTask(task) // 執行任務, 這裏面可能會觸發新的Web API調用
}
if (hasAnyPendingTask()) {
sleep() // 睡一覺,有新任務推送到回調隊列時時再喚醒我哦
} else {
break // 終止程序, 沒什麼好乾的拜拜了
}
}
複製代碼
咱們經過setTimeout Web API這個例子一步一步看看上述的一切是怎麼運做的。setTimeout Web API主要用於延時執行一些操做,可是回調真正被執行, 須要等待當前程序執行完畢(即棧爲空), 也就是說,setTimeout函數回調執行時間未必等於你指定的延時時間。setTimeout的語法以下:
setTimeout(callbackFunction, timeInMilliseconds);
複製代碼
callbackFunction是一個回調函數,它將會在timeInMilliseconds以後執行. 咱們修改上面的代碼來調用setTimeout:
function printHello() {
console.log('Hello from baz');
}
function baz() {
setTimeout(printHello, 3000);
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
複製代碼
上面的代碼延時調用了console.log. 棧仍是會像以前同樣,如foo() => bar() => baz()
, 當baz開始執行併到達setTimeout時,Javascript會將回調函數傳遞給Web API,而且繼續執行下一行。 由於這裏沒有下一行了,棧會彈出baz,接着彈出bar和foo。
在這期間,Web API正在進行3s等待,當時間到達時,它會將回調推動回調隊列中。 由於這時候調用棧爲空,事件循環會將這個回調推動棧中,並執行這個回調。
🎉🎉Philip Robers建立了一個神奇的在線工具Loupe,來可視化Javascript的底層運行。上面的實例能夠查看這個連接🎉🎉
因此說咱們Javascript是單線程的,可是不少Web API的執行是多線程的。也就是說Javascript的單線程指的是‘Javascript代碼’的執行是單線程.
經過Node.js咱們能夠作更多的事情, 而不只限於瀏覽器的端。那麼它是怎麼運做的?
Node.js 和Chrome同樣,一樣使用Google的V8引擎來提供Javascript運行時. 它使用libuv(C++編寫)來和V8的事件循環配合,擴展更多能夠在後臺執行的東西, 好比文件系統I/O, 網絡I/O。Node的標準庫API遵循了瀏覽器Web API的相似回調風格。
若是你比較了瀏覽器和node的結構圖,你會發現二者很是類似。右側的部分相似於Web API,一樣包含事件隊列(或者稱爲回調隊列/消息隊列)和事件循環。
V八、事件循環、事件隊列都在單線程中運行,最右側還有工做線程(Worker Thread)負責提供異步的I/O操做。這就是爲何說Node.js擁有非阻塞的、事件驅動的異步I/O架構。
上面的內容都來源於Philip Roberts30min的高光演講(五年前)
本文完