出處:https://segmentfault.com/a/1190000019205065
JavaScript引擎是如何工作的?從調用棧到Promise你需要知道的一切
翻譯:瘋狂的技術宅
本文首發微信公眾號:前端先鋒
你有沒有想過瀏覽器是如何讀取和運行JavaScript 代碼的嗎?這看起來很神奇,但你可以學到一些發生在幕後的事情。讓我們通過介紹JavaScript 引擎的精彩世界在這種語言中盡情暢遊。
在Chrome 中打開瀏覽器控制台,然後查看“Sources”標籤。你會看到一個有趣的命名:
什麼是調用棧(Call Stack)?看上去像是有很多東西正在運行,即使是只執行幾行代碼也是如此。實際上,並不是在所有Web 瀏覽器上都能對JavaScript 做到開箱即用。
有一個很大的組件來 最受歡迎的JavaScript 引擎是V8,在Google Chrome 和Node.js 中使用,SpiderMonkey 用於Firefox,以及Safari/WebKit 所使用的JavaScriptCore。
今天的JavaScript 引擎是個很傑出的工程,儘管它不可能覆蓋瀏覽器工作的方方面面,但是每個引擎都有一些較小的部件在為我們努力工作。
其中一個組件是 你準備好迎接他們了嗎?
JavaScript 引擎和全局內存
我認為JavaScript 既是編譯型語言又是解釋型語言。信不信由你,JavaScript 引擎在執行之前實際上編譯了你的代碼。
是不是聽起來很神奇?這種魔術被稱為JIT(即時編譯)。它本身就是一個很大的話題,即使是一本書也不足以描述JIT 的工作原理。但是現在我們可以跳過編譯背後的理論,
先看以下代碼:
var
function
}
如果我問你 你會說些什麼?你可能會說“瀏覽器讀取代碼”或“瀏覽器執行代碼”。
現實中比那更加微妙。首先不是瀏覽器而是引擎讀取該代碼片段。JavaScript引擎讀取代碼
全局內存(也稱為堆)所以回到前面的例子,當引擎讀取上面的代碼時,全局內存中被填充了兩個綁定:
即使例子中只有變量和函數,也要考慮你的JavaScript 代碼在更大的環境中運行:瀏覽器或在Node.js 中。在這些環境中,有許多預定義的函數和變量,被稱為全局。全局內存將比num 和pow 所佔用的空間更多。記住這一點。
此時沒有執行任何操作,但是如果嘗試像這樣運行我們的函數會怎麼樣:
var
}
pow
將會發生什麼?現在事情變得有趣了。當一個函數被調用時,JavaScript 引擎會為另外兩個盒子騰出空間:
- 全局執行上下文環境
- 調用棧
全局執行上下文和調用棧
在上一節你了解了JavaScript 引擎是如何讀取變量和函數聲明的,他們最終進入了全局內存(堆)。
但是現在我們執行了一個JavaScript 函數,引擎必須要處理它。怎麼處理?每個JavaScript 引擎都有一個
調用棧是一個棧數據結構 JavaScript 函數就是這樣的。
當函數開始執行時,如果被某些其他函數卡住,那麼它無法離開調用堆棧。請注意,因為
但是現在讓我們回到上面的例子。當調用該函數時,引擎會將該函數壓入調用堆棧
我喜歡將 如果還沒有先吃掉頂部的所有薯片,就吃不到到底部的薯片!幸運的是我們的函數是同步的:它是一個簡單的乘法,可以很快的得到計算結果。
同時,引擎還分配了 這是它的樣子:
想像一下 多麼美好!但這只是故事的一半。如果函數有一些嵌套變量或一個或多個內部函數怎麼辦?
即使在下面的簡單變體中,JavaScript 引擎也會創建
var
}
pow
請注意,我在函數pow 中添加了一個名為fixed 的變量。在這種情況下,本地執行上下文中將包含一個用於保持固定的框。我不太擅長在小方框裡畫更小的框!你現在必須運用自己的想像力。
本地執行上下文將出現在pow 附近,包含在全局執行上下文中的綠色框內。你還可以想像,對於嵌套函數中的每個嵌套函數,引擎都會創建更多的本地執行上下文 這些框可以很快的到達它們該去的地方。
單線程的JavaScript
我們說 也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。
當處理 例如,計算兩個數字的和就是同步的,並且以微秒做為運行單位。但是當進行網絡通信和與外界的互動時呢?
幸運的是 即使他們一次可以執行一個函數,也有一種方法可以讓外部實體執行較慢的函數:在我們的例子中是瀏覽器。我們稍後會探討這個話題。
這時,你應該了解到
- 使用
- 將每個函數調用送到
- 創建一個
- 創建了許多微小的
到此為止,你腦子裡應該有了一個 在接下來的部分中,你將看到
異步JavaScript,回調隊列和事件循環
全局內存、執行上下文和調用棧解釋了同步JavaScript 代碼在瀏覽器中的運行方式。然而我們還錯過了一些東西。當有異步函數運行時會發生什麼?
我所指的異步函數是每次與外界的互動都需要一些時間才能完成的函數 例如調用 現在的JavaScript 引擎都有辦法處理這種函數而不會阻塞調用堆棧,瀏覽器也是如此。
請記住,調用堆棧一次只可以執行一個函數,幸運的是,JavaScript 引擎非常智能,並且能在瀏覽器的幫助下解決問題。
當我們運行異步函數時,瀏覽器會接受該函數並運行它 考慮下面的計時器:
setTimeout ){
}
你肯定多次見到過 即當JavaScript 誕生時,語言中並沒有內置的setTimeout。
實際上setTimeout 是所謂的 多麼體貼!這在實踐中意味著什麼?由於setTimeout 是一個瀏覽器API,該函數由瀏覽器直接運行(它會暫時出現在調用棧中,但會立即刪除)。
然後10 秒後瀏覽器接受我們傳入的回調函數並將其移動到 此時我們的JavaScript 引擎中還有兩個框。請看以下代碼:
var
}
pow ){
}
可以這樣畫完成我們的圖:
如你所見 10秒後,計時器被觸發,回調函數準備好運行。但首先它必須通過回調隊列 回調隊列是一個隊列數據結構,顧名思義是一個有序的函數隊列。
每個 但誰推動了這個函數呢?還有另一個名為
Event Loop 現在只做一件事:它應檢查調用棧是否為空 如果回調隊列中有一些函數,並且如果調用棧是空閒的,那麼這時應將回調送到調用棧。在完成後執行該函數。
這是用於
想像一下,callback() 已準備好執行。當pow() 完成時,就是這樣!即使我簡化了一些東西,如果你理解了上面的圖,那麼就可以理解JavaScript 的一切了。
請記住:
如果你喜歡視頻,我建議去看Philip Roberts 的視頻:事件循環是什麼。這是關於時間循環的最好的解釋之一。
堅持下去,因為我們還沒有使用異步JavaScript。在後面的內容中,我們將詳細介紹ES6 Promises。
回調地獄和ES6 的Promise
JavaScript 中的回調函數無處不在 它們用於同步和異步代碼。例如map 方法:
function
}[
mapper 是在map 中傳遞的回調函數。上面的代碼是同步的。但要考慮一個間隔:
function ){
}
setInterval
該代碼是異步的,我們在setInterval 中傳遞了回調runMeEvery。回調在JavaScript 中很普遍,所以近幾年裡出現了一個問題:回調地獄。
JavaScript中的回調地獄 正是由於JavaScript 的異步性質導致程序員掉進了這個陷阱。
說實話,我從來沒有碰到過極端的回調金字塔,也許是因為我重視代碼的可讀性,並且總是試著堅持這個原則。如果你發現自己掉進了回調地獄,那就說明你的函數太多了。
我不會在這裡討論回調地獄,如果你很感興趣的話,給你推荐一個網站:我們現在要關注的是 ES6 Promise 是對JavaScript 語言的補充,旨在解決可怕的回調地獄。但Promise 是什麼?
JavaScript Promise 是未來事件的表示 Promise 能夠以success 結束:用行話說就是它已經 但如果Promise 出錯,我們會說它處於 Promise 也有一個默認狀態:每個新Promise都以
創建和使用Promise
要創建新的Promise,可以通過將回調函數傳給要調用的Promise 構造函數的方法。回調函數可以使用兩個參數:讓我們創建一個新的Promise,它將在5秒後resolve(你可以在瀏覽器的控制台中嘗試這些例子):
const ){},
});
如你所見,resolve 是一個函數,我們調用它使Promise 成功。下面的例子中reject 將得到rejected 的Promise:
const ){},
});
請注意,在第一個示例中,你可以省略reject ,因為它是第二個參數。但是如果你打算使用reject 換句話說,以下代碼將無法工作,最終將以resolved 的Promise 結束:
// Can't omit resolve !
){},
});
現在Promise 看起來不是那麼有用。這些例子不向用戶打印任何內容。讓我們添加一些數據。resolved 的和rejected 的Promises 都可以返回數據 這是一個例子:
const
});
但我們仍然看不到任何數據。要從Promise 中提取數據,你還需要一個名為then 的方法 它需要一個回調(真是具有諷刺意味!)來接收實際的數據:
const
});myPromise.
});
作為JavaScript 開發人員,你將主要與來自外部的Promises 進行交互。相反,庫的開發者更有可能將遺留代碼包裝在Promise 構造函數中,如下所示:
const
在需要時,我們還可以通過調用Promise.resolve() 來創建和解決Promise:
Promise.
所以回顧一下,事件以掛起狀態開始,可以成功(resolved,fulfilled)或失敗(rejected)Promise 可以返回數據,通過把then 附加到Promise 來提取數據。在下一節中,我們將看到如何處理來自Promise 的錯誤。
ES6 Promise 中的錯誤處理
JavaScript 中的錯誤處理一直很簡單,至少對於同步代碼而言。請看下面的例子:
function ) {
}
try}
}
輸出將是:
Catching the error! Error: Sorry mate!
錯誤在catch 塊中被捕獲。現在讓我們嘗試使用異步函數:
function ) {
}
try}
}
由於setTimeout,上面的代碼是異步的。如果運行它會發生什麼?
throw
^
Errorat
這次輸出是不同的。錯誤沒有通過catch塊。它可以自由地在棧中傳播。
那是因為try/catch 僅適用於同步代碼。如果你感到好奇,可以在
幸運的是,Promise 有一種處理異步錯誤的方法,就像它們是同步的一樣。
const
});
在上面的例子中,我們可以用catch 處理程序錯誤,再次採取回調:
const
});myPromise.
我們也可以調用Promise.reject() 來創建和reject Promise:
Promise
ES6 Promise 組合器:Promise.all,Promise.allSettled,Promise.any和它們的小伙伴
Promise 並不是在孤軍奮戰。Promise API 提供了一系列 其中最有用的是 問題是當任何一個Promise rejected時,Promise.all 就會rejects 。
Promise.race 如果其中一個Promise rejects ,它仍然會rejects。
較新版本的V8 也將實現兩個新的組合器:Promise.any
Promise.any 可以表明任何Promise 是否fullfilled。與Promise.race 的區別在於
最有趣的是 它仍然需要一系列的Promise,但 當你想要檢查Promise 數組中是否全部已解決時,它是有用的。可以認為它總是和Promise.all 對著幹。
ES6 Promise 和microtask 隊列
如果你還記得前面的章節 但是
你應該注意一個有趣的現象:當事件循環檢查是否有任何新的回調準備好被推入調用棧時,來自微任務隊列的回調具有優先權。
異步的進化:從Promise 到async/await
JavaScript 正在快速發展,每年我們都會不斷改進語言。Promise 似乎是到達了終點,但
async/await 只是一種風格上的改進,我們稱之為語法糖。async/await 不會以任何方式改變JavaScript(請記住,JavaScript 必須向後兼容舊瀏覽器,不應破壞現有代碼)。
它只是一種 讓我們舉個例子。之前我們用then 的Promise:
const
});myPromise.
現在 我們可以將Promise 包裝在標記為async 的函數中,然後等待結果:
const
});
async ) {
}
getData
現在有趣的是異步函數將始終返回Promise,並且沒人能阻止你這樣做:
async ) {
}
getData
怎麼處理錯誤呢?async/await 提供的一個好處就是有機會使用 (參見 讓我們再看一下Promise,我們使用catch處理程序來處理錯誤:
const
});myPromise.
使用異步函數,我們可以重構以下代碼:
async ) {
}
}
getData
不是每個人都會用這種風格。try/catch 會使你的代碼混亂。雖然用try/catch還有另一個問題要指出。請看以下代碼,在try塊中引發錯誤:
async ) {
}}
}
}
getData. .
哪一字符串會打印到控制台?請記住,他們在兩條不同的軌道上行駛,就像兩列火車。但他們永遠不會碰面!也就是說,throw 引發的錯誤永遠不會觸發getData() 的catch 處理程序。運行上面的代碼將導致“抓住我,如果你可以”,然後“不管怎樣我都會跑!”。
實際上我們不希望throw 觸發當前的處理。一種可能的解決方案是從函數返回Promise.reject():
async ) {
}}
}
}
現在錯誤將按預期處理:
getData. .
除此之外 我們可以更好地控制錯誤處理,代碼看起來更清晰
我不建議把所有的JavaScript 代碼都重構為async/await。這必須是與團隊討論之後的選擇。但是如果你自己工作的話,無論你使用簡單的Promise 還是async/await 都是屬於個人偏好的問題。
總結
JavaScript 在最流行的JavaScript 引擎中,有Google Chrome 和Node.js 使用的
JavaScript 引擎有很多部分組成:所有這些部分在完美的調整中協同工作,以便在JavaScript 中處理同步和異步代碼。
JavaScript 引擎是單線程的 這種限制是JavaScript 異步性質的基礎:所有需要時間的操作必須由外部實體(例如瀏覽器)或
為了簡化異步代碼流程,Promise 是一個異步對象,用於表示異步操作的失敗或成功。但改進並沒有止步於此。2017年async/await誕生了
其它高讚文章:
- 12個令人驚嘆的CSS實驗項目
- 必須要會的50 個React 面試題
- 世界頂級公司的前端面試都問些什麼
- 11 個最好的JavaScript 動態效果庫
- CSS Flexbox 可視化手冊
- 從設計者的角度看React
- 過節很無聊?還是用JavaScript 寫一個腦力小遊戲吧!
- CSS粘性定位是怎樣工作的
- 一步步教你用HTML5 SVG實現動畫效果
- 程序員30歲前月薪達不到30K,該何去何從
- 14個最好的JavaScript 數據可視化庫
- 8 個給前端的頂級VS Code 擴展插件
- Node.js 多線程完全指南
- 把HTML轉成PDF的4個方案及實現
沒有留言:
張貼留言