摘要:最受歡迎的引擎是,在和中使用,用于,以及所使用的。怎么處理每個引擎都有一個基本組件,稱為調用棧。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。每個異步函數在被送入調用棧之前必須通過回調隊列。例如方法是在中傳遞的回調函數。
?
翻譯:瘋狂的技術宅
原文:www.valentinog.com/blog/engine…
從Call Stack,Global Memory,Event Loop,Callback Queue到 Promises 和 Async/Await 的 JavaScript引擎之旅!
?
你有沒有想過瀏覽器是如何讀取和運行 JavaScript 代碼的嗎?這看起來很神奇,但你可以學到一些發生在幕后的事情。讓我們通過介紹 JavaScript 引擎的精彩世界在這種語言中盡情暢游。
在 Chrome 中打開瀏覽器控制臺,然后查看“Sources”標簽。你會看到一個有趣的命名:Call Stack(在Firefox中,你可以在代碼中插入一個斷點后看到調用棧):
?
什么是調用棧(Call Stack)?看上去像是有很多東西正在運行,即使是只執行幾行代碼也是如此。實際上,并不是在所有 Web 瀏覽器上都能對 JavaScript 做到開箱即用。
有一個很大的組件來編譯和解釋我們的 JavaScript 代碼:它就是 JavaScript 引擎。最受歡迎的 JavaScript 引擎是V8,在 Google Chrome 和 Node.js 中使用,SpiderMonkey 用于 Firefox,以及 Safari/WebKit 所使用的 JavaScriptCore。
今天的 JavaScript 引擎是個很杰出的工程,盡管它不可能覆蓋瀏覽器工作的方方面面,但是每個引擎都有一些較小的部件在為我們努力工作。
其中一個組件是調用棧,它與全局內存和執行上下文一起運行我們的代碼。你準備好迎接他們了嗎?
JavaScript 引擎和全局內存
我認為 JavaScript 既是編譯型語言又是解釋型語言。信不信由你,JavaScript 引擎在執行之前實際上編譯了你的代碼。
是不是聽起來很神奇?這種魔術被稱為 JIT(即時編譯)。它本身就是一個很大的話題,即使是一本書也不足以描述 JIT 的工作原理。但是現在我們可以跳過編譯背后的理論,專注于執行階段,這仍然是很有趣的。
先看以下代碼:
?
var num = 2; function pow(num) { return num * num; }
如果我問你如何在瀏覽器中處理上述代碼?你會說些什么?你可能會說“瀏覽器讀取代碼”或“瀏覽器執行代碼”。
現實中比那更加微妙。首先不是瀏覽器而是引擎讀取該代碼片段。 JavaScript引擎讀取代碼,當遇到第一行時,就會將一些引用放入全局內存中。
**全局內存(也稱為堆)**是 JavaScript 引擎用來保存變量和函數聲明的區域。所以回到前面的例子,當引擎讀取上面的代碼時,全局內存中被填充了兩個綁定:
?
即使例子中只有變量和函數,也要考慮你的 JavaScript 代碼在更大的環境中運行:瀏覽器或在 Node.js 中。在這些環境中,有許多預定義的函數和變量,被稱為全局。全局內存將比 num 和 pow 所占用的空間更多。記住這一點。
此時沒有執行任何操作,但是如果嘗試像這樣運行我們的函數會怎么樣:
?
var num = 2; function pow(num) { return num * num; } pow(num);
將會發生什么?現在事情變得有趣了。當一個函數被調用時,JavaScript 引擎會為另外兩個盒子騰出空間:
全局執行上下文環境
調用棧
全局執行上下文和調用棧
在上一節你了解了 JavaScript 引擎是如何讀取變量和函數聲明的,他們最終進入了全局內存(堆)。
但是現在我們執行了一個 JavaScript 函數,引擎必須要處理它。怎么處理?每個 JavaScript 引擎都有一個基本組件,稱為調用棧。
調用棧是一個棧數據結構:這意味著元素可以從頂部進入,但如果在它們上面還有一些元素,就不能離開棧。 JavaScript 函數就是這樣的。
當函數開始執行時,如果被某些其他函數卡住,那么它無法離開調用堆棧。請注意,因為這個概念有助于理解“JavaScript是單線程”這句話。
但是現在讓我們回到上面的例子。 當調用該函數時,引擎會將該函數壓入調用堆棧中:
?
我喜歡將調用棧看作是一疊薯片。如果還沒有先吃掉頂部的所有薯片,就吃不到到底部的薯片!幸運的是我們的函數是同步的:它是一個簡單的乘法,可以很快的得到計算結果。
同時,引擎還分配了全局執行上下文,這是 JavaScript 代碼運行的全局環境。這是它的樣子:
?
想象一下全局執行環境作為一個海洋,其中 JavaScript 全局函數就像魚一樣在里面游泳。多么美好!但這只是故事的一半。如果函數有一些嵌套變量或一個或多個內部函數怎么辦?
即使在下面的簡單變體中,JavaScript 引擎也會創建本地執行上下文:
?
var num = 2; function pow(num) { var fixed = 89; return num * num; } pow(num);
請注意,我在函數 pow 中添加了一個名為 fixed 的變量。在這種情況下,本地執行上下文中將包含一個用于保持固定的框。我不太擅長在小方框里畫更小的框!你現在必須運用自己的想象力。
本地執行上下文將出現在 pow 附近,包含在全局執行上下文中的綠色框內。 你還可以想象,對于嵌套函數中的每個嵌套函數,引擎都會創建更多的本地執行上下文。這些框可以很快的到達它們該去的地方。
單線程的JavaScript
我們說 JavaScript 是單線程的,因為有一個調用棧處理我們的函數。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。
當處理同步代碼時,這不是什么問題。例如,計算兩個數字的和就是同步的,并且以微秒做為運行單位。但是當進行網絡通信和與外界的互動時呢?
幸運的是 JavaScript引擎被默認設計為異步。即使他們一次可以執行一個函數,也有一種方法可以讓外部實體執行較慢的函數:在我們的例子中是瀏覽器。我們稍后會探討這個話題。
這時,你應該了解到當瀏覽器加載某些 JavaScript 代碼時,引擎會逐行讀取并執行以下步驟:
使用變量和函數聲明填充全局內存(堆)
將每個函數調用送到調用棧
創建一個全局執行上下文,其在中執行全局函數
創建了許多微小的本地執行上下文(如果有內部變量或嵌套函數)
到此為止,你腦子里應該有了一個 JavaScript 引擎同步機制的全景圖。在接下來的部分中,你將看到異步代碼如何在 JavaScript 中工作以及為什么這樣工作。
異步JavaScript,回調隊列和事件循環
全局內存、執行上下文和調用棧解釋了同步 JavaScript 代碼在瀏覽器中的運行方式。然而我們還錯過了一些東西。當有異步函數運行時會發生什么?
我所指的異步函數是每次與外界的互動都需要一些時間才能完成的函數。例如調用 REST API 或調用計時器是異步的,因為它們可能需要幾秒鐘才能運行完畢。 現在的 JavaScript 引擎都有辦法處理這種函數而不會阻塞調用堆棧,瀏覽器也是如此。
請記住,調用堆棧一次只可以執行一個函數,**甚至一個阻塞函數都可以直接凍結瀏覽器。**幸運的是,JavaScript 引擎非常智能,并且能在瀏覽器的幫助下解決問題。
當我們運行異步函數時,瀏覽器會接受該函數并運行它。考慮下面的計時器:
?
setTimeout(callback, 10000); function callback(){ console.log("hello timer!"); }
你肯定多次見到過 setTimeout ,但是你可能不知道它不是一個內置的 JavaScript 函數。即當 JavaScript 誕生時,語言中并沒有內置的 setTimeout。
實際上 setTimeout 是所謂的 Browser API 的一部分,它是瀏覽器提供給我們的便利工具的集合。多么體貼!這在實踐中意味著什么?由于 setTimeout 是一個瀏覽器 API,該函數由瀏覽器直接運行(它會暫時出現在調用棧中,但會立即刪除)。
然后 10 秒后瀏覽器接受我們傳入的回調函數并將其移動到回調隊列。此時我們的 JavaScript 引擎中還有兩個框。請看以下代碼:
?
var num = 2; function pow(num) { return num * num; } pow(num); setTimeout(callback, 10000); function callback(){ console.log("hello timer!"); }
可以這樣畫完成我們的圖:
?
如你所見 setTimeout 在瀏覽器上下文中運行。 10秒后,計時器被觸發,回調函數準備好運行。但首先它必須通過回調隊列。回調隊列是一個隊列數據結構,顧名思義是一個有序的函數隊列。
每個**異步函數在被送入調用棧之前必須通過回調隊列。**但誰推動了這個函數呢?還有另一個名為 Event Loop 的組件。
Event Loop 現在只做一件事:它應檢查調用棧是否為空。如果回調隊列中有一些函數,并且如果調用棧是空閑的,那么這時應將回調送到調用棧。在完成后執行該函數。
這是用于處理異步和同步代碼的 JavaScript 引擎的大圖:
?
想象一下,callback() 已準備好執行。當 pow() 完成時,**調用棧為空,事件循環推送 **callback()。就是這樣!即使我簡化了一些東西,如果你理解了上面的圖,那么就可以理解 JavaScript 的一切了。
請記住:Browser API、回調隊列和事件循環是異步 JavaScript 的支柱。
如果你喜歡視頻,我建議去看 Philip Roberts 的視頻:事件循環是什么。這是關于時間循環的最好的解釋之一。
堅持下去,因為我們還沒有使用異步 JavaScript。在后面的內容中,我們將詳細介紹 ES6 Promises。
回調地獄和 ES6 的 Promise
JavaScript 中的回調函數無處不在。它們用于同步和異步代碼。例如 map 方法:
?
function mapper(element){ return element * 2; } [1, 2, 3, 4, 5].map(mapper);
mapper 是在 map 中傳遞的回調函數。上面的代碼是同步的。但要考慮一個間隔:
?
function runMeEvery(){ console.log("Ran!"); } setInterval(runMeEvery, 5000);
該代碼是異步的,我們在 setInterval 中傳遞了回調 runMeEvery。回調在 JavaScript 中很普遍,所以近幾年里出現了一個問題:回調地獄。
JavaScript中的回調地獄指的是編程的“風格”,回調嵌套在嵌套在…...其他回調中的回調中。正是由于 JavaScript 的異步性質導致程序員掉進了這個陷阱。
說實話,我從來沒有碰到過極端的回調金字塔,也許是因為我重視代碼的可讀性,并且總是試著堅持這個原則。如果你發現自己掉進了回調地獄,那就說明你的函數太多了。
我不會在這里討論回調地獄,如果你很感興趣的話,給你推薦一個網站: callbackhell.com 更深入地探討了這個問題并提供了一些解決方案。我們現在要關注的是 ES6 Promise。 ES6 Promise 是對 JavaScript 語言的補充,旨在解決可怕的回調地獄。但 Promise 是什么?
JavaScript Promise 是未來事件的表示。Promise 能夠以 success 結束:用行話說就是它已經 resolved(已經完成)。但如果 Promise 出錯,我們會說它處于rejected狀態。 Promise 也有一個默認狀態:每個新Promise都以 pending 狀態開始。
創建和使用 Promise
要創建新的 Promise,可以通過將回調函數傳給要調用的 Promise 構造函數的方法。回調函數可以使用兩個參數:resolve 和 reject。讓我們創建一個新的 Promise,它將在5秒后 resolve(你可以在瀏覽器的控制臺中嘗試這些例子):
?
const myPromise = new Promise(function(resolve){ setTimeout(function(){ resolve() }, 5000) });
如你所見,resolve 是一個函數,我們調用它使 Promise 成功。下面的例子中 reject 將得到 rejected 的 Promise:
?
const myPromise = new Promise(function(resolve, reject){ setTimeout(function(){ reject() }, 5000) });
請注意,在第一個示例中,你可以省略 reject ,因為它是第二個參數。但是如果你打算使用 reject**,就不能省略 resolve**。換句話說,以下代碼將無法工作,最終將以 resolved 的 Promise 結束:
?
// Can"t omit resolve ! const myPromise = new Promise(function(reject){ setTimeout(function(){ reject() }, 5000) });
現在 Promise 看起來不是那么有用。這些例子不向用戶打印任何內容。讓我們添加一些數據。 resolved 的和rejected 的 Promises 都可以返回數據。這是一個例子:
?
const myPromise = new Promise(function(resolve) { resolve([{ name: "Chris" }]); });
但我們仍然看不到任何數據。 要從 Promise 中提取數據,你還需要一個名為 then 的方法。它需要一個回調(真是具有諷刺意味!)來接收實際的數據:
?
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then(function(data) { console.log(data); });
作為 JavaScript 開發人員,你將主要與來自外部的 Promises 進行交互。相反,庫的開發者更有可能將遺留代碼包裝在 Promise 構造函數中,如下所示:
?
const shinyNewUtil = new Promise(function(resolve, reject) { // do stuff and resolve // or reject });
在需要時,我們還可以通過調用 Promise.resolve() 來創建和解決 Promise:
?
Promise.resolve({ msg: "Resolve!"}) .then(msg => console.log(msg));
所以回顧一下,JavaScript Promise 是未來發生的事件的書簽。事件以掛起狀態開始,可以成功(resolved,fulfilled)或失敗(rejected)。 Promise 可以返回數據,通過把 then 附加到 Promise 來提取數據。在下一節中,我們將看到如何處理來自 Promise 的錯誤。
ES6 Promise 中的錯誤處理
JavaScript 中的錯誤處理一直很簡單,至少對于同步代碼而言。請看下面的例子:
?
function makeAnError() { throw Error("Sorry mate!"); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); }
輸出將是:
?
Catching the error! Error: Sorry mate!
錯誤在 catch 塊中被捕獲。現在讓我們嘗試使用異步函數:
?
function makeAnError() { throw Error("Sorry mate!"); } try { setTimeout(makeAnError, 5000); } catch (error) { console.log("Catching the error! " + error); }
由于 setTimeout,上面的代碼是異步的。如果運行它會發生什么?
?
throw Error("Sorry mate!"); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)
這次輸出是不同的。錯誤沒有通過 catch塊。它可以自由地在棧中傳播。
那是因為 try/catch 僅適用于同步代碼。如果你感到好奇,可以在 Node.js 中的錯誤處理中得到該問題的詳細解釋。
幸運的是,Promise 有一種處理異步錯誤的方法,就像它們是同步的一樣。
?
const myPromise = new Promise(function(resolve, reject) { reject("Errored, sorry!"); });
在上面的例子中,我們可以用 catch 處理程序錯誤,再次采取回調:
?
const myPromise = new Promise(function(resolve, reject) { reject("Errored, sorry!"); }); myPromise.catch(err => console.log(err));
我們也可以調用 Promise.reject() 來創建和 reject Promise:
?
Promise.reject({msg: "Rejected!"}).catch(err => console.log(err));
ES6 Promise 組合器:Promise.all,Promise.allSettled,Promise.any和它們的小伙伴
Promise 并不是在孤軍奮戰。 Promise API 提供了一系列將 Promise 組合在一起的方法。其中最有用的是Promise.all,它接受一系列 Promise 并返回一個Promise。問題是當任何一個Promise rejected時,Promise.all 就會 rejects 。
Promise.race 在數組中的一個 Promise 結束后立即 resolves 或 reject。如果其中一個Promise rejects ,它仍然會rejects。
較新版本的 V8 也將實現兩個新的組合器:**Promise.allSettled **和 Promise.any。 Promise.any 仍處于提案的早期階段:在撰寫本文時,還不支持。
Promise.any 可以表明任何 Promise 是否 fullfilled。與 Promise.race 的區別在于 Promise.any 不會 reject,即使是其中一個Promise 被 rejected。
最有趣的是 Promise.allSettled。它仍然需要一系列的 Promise,但如果其中一個 Promise rejects 的話 ,它不會被短路。當你想要檢查 Promise 數組中是否全部已解決時,它是有用的。可以認為它總是和 Promise.all 對著干。
ES6 Promise 和 microtask 隊列
如果你還記得前面的章節**,JavaScript 中的每個異步回調函數都會在被推入調用棧之前在回調隊列中結束**。但是在 Promise 中傳遞的回調函數有不同的命運:它們由微任務隊列處理,而不是由回調隊列處理。
你應該注意一個有趣的現象:微任務隊列優先于回調隊列。當事件循環檢查是否有任何新的回調準備好被推入調用棧時,來自微任務隊列的回調具有優先權。
Jake Archibald 在任務、微任務、隊列和時間表一文中更詳細地介紹了這些機制,這是一篇很棒的文章。
異步的進化:從 Promise 到 async/await
JavaScript 正在快速發展,每年我們都會不斷改進語言。Promise 似乎是到達了終點,但 **ECMAScript 2017(ES8)的新語法誕生了:async / await **。
async/await 只是一種風格上的改進,我們稱之為語法糖。 async/await 不會以任何方式改變 JavaScript(請記住,JavaScript 必須向后兼容舊瀏覽器,不應破壞現有代碼)。
它只是一種基于 Promise 編寫異步代碼的新方法。讓我們舉個例子。之前我們用 then 的 Promise:
?
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then((data) => console.log(data))
現在使用async/await,我們可以從另一個角度看待用同步的方式處理異步代碼。我們可以將 Promise 包裝在標記為 async 的函數中,然后等待結果:
?
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); async function getData() { const data = await myPromise; console.log(data); } getData();
現在有趣的是異步函數將始終返回 Promise,并且沒人能阻止你這樣做:
?
async function getData() { const data = await myPromise; return data; } getData().then(data => console.log(data));
怎么處理錯誤呢? async/await 提供的一個好處就是有機會使用 try/catch。 (參見異步函數中的異常處理及測試方法 )。讓我們再看一下Promise,我們使用catch處理程序來處理錯誤:
?
const myPromise = new Promise(function(resolve, reject) { reject("Errored, sorry!"); }); myPromise.catch(err => console.log(err));
使用異步函數,我們可以重構以下代碼:
?
async function getData() { try { const data = await myPromise; console.log(data); // or return the data with return data } catch (error) { console.log(error); } } getData();
不是每個人都會用這種風格。 try/catch 會使你的代碼混亂。雖然用 try/catch還有另一個問題要指出。請看以下代碼,在try塊中引發錯誤:
?
async function getData() { try { if (true) { throw Error("Catch me if you can"); } } catch (err) { console.log(err.message); } } getData() .then(() => console.log("I will run no matter what!")) .catch(() => console.log("Catching err"));
哪一字符串會打印到控制臺?請記住,try/catch是一個同步構造,但我們的異步函數會產生一個 Promise。他們在兩條不同的軌道上行駛,就像兩列火車。但他們永遠不會碰面!也就是說,throw 引發的錯誤永遠不會觸發 getData() 的 catch 處理程序。運行上面的代碼將導致 “抓住我,如果你可以”,然后“不管怎樣我都會跑!”。
實際上我們不希望 throw 觸發當前的處理。一種可能的解決方案是從函數返回 Promise.reject():
?
async function getData() { try { if (true) { return Promise.reject("Catch me if you can"); } } catch (err) { console.log(err.message); } }
現在錯誤將按預期處理:
?
getData() .then(() => console.log("I will NOT run no matter what!")) .catch(() => console.log("Catching err")); "Catching err" // output
除此之外 async/await 似乎是在 JavaScript 中構建異步代碼的最佳方式。我們可以更好地控制錯誤處理,代碼看起來更清晰。
我不建議把所有的 JavaScript 代碼都重構為 async/await。這必須是與團隊討論之后的選擇。但是如果你自己工作的話,無論你使用簡單的 Promise 還是 async/await 都是屬于個人偏好的問題。
總結
JavaScript 是一種用于Web的腳本語言,具有先被編譯然后再由引擎解釋的特性。在最流行的 JavaScript 引擎中,有 Google Chrome 和 Node.js 使用的V8,為網絡瀏覽器 Firefox 構建的 SpiderMonkey,由Safari使用的 JavaScriptCore。
JavaScript 引擎有很多部分組成:調用棧、全局內存、事件循環和回調隊列。所有這些部分在完美的調整中協同工作,以便在 JavaScript 中處理同步和異步代碼。
JavaScript 引擎是單線程的,這意味著只有一個用于運行函數的調用堆棧。這種限制是 JavaScript 異步性質的基礎:所有需要時間的操作必須由外部實體(例如瀏覽器)或回調函數負責。
為了簡化異步代碼流程,ECMAScript 2015 給我們帶來了 Promises。 Promise 是一個異步對象,用于表示異步操作的失敗或成功。但改進并沒有止步于此。 2017年 async/await誕生了:它是 Promise 的一種風格上的彌補,可以用來編寫異步代碼,就好像它是同步的一樣。
歡迎關注前端公眾號:前端先鋒,獲取前端工程化實用工具包。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/6686.html
摘要:最受歡迎的引擎是,在和中使用,用于,以及所使用的。單線程的我們說是單線程的,因為有一個調用棧處理我們的函數。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。每個異步函數在被送入調用棧之前必須通過回調隊列。 翻譯:瘋狂的技術宅原文:https://www.valentinog.com/bl... 本文首發微信公眾號:前端先鋒歡迎關注,每天都給你推送新鮮的前端技術文章 sh...
摘要:最受歡迎的引擎是,由和使用,用于,以及使用的。引擎它們是如何工作的全局執行上下文和調用堆棧剛剛了解了引擎如何讀取變量和函數聲明,它們最終被放入了全局內存堆中。事件循環只有一個任務它檢查調用堆棧是否為空。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 有沒有想過瀏覽器如何讀取和運行JS代碼? 這看起來很神奇,我們可以通過瀏覽...
摘要:事件循環從回調隊列中獲取并將其推入調用堆棧。執行從調用堆棧中移除從調用堆棧中移除快速回顧值得注意的是,指定了事件循環應該如何工作,這意味著在技術上它屬于引擎的職責范圍,不再僅僅扮演宿主環境的角色。 此篇是 JavaScript是如何工作的第四篇,其它三篇可以看這里: JavaScript是如何工作的:引擎,運行時和調用堆棧的概述! JavaScript是如何工作的:深入V8引擎&編寫...
摘要:事件循環從回調隊列中獲取并將其推送到調用堆棧。如何工作請注意,不會自動將您的回調函數放到事件循環隊列中。它設置了一個計時器,當計時器到期時,環境將您的回調函數放入事件循環中,以便將來的某個事件會將其選中并執行它。 我們將通過回顧第一篇文章中單線程編程的缺點,然后在討論如何克服它們來構建令人驚嘆的JavaScript UI。在文章結尾處,我們將分享5個關于如何使用async / awai...
showImg(https://segmentfault.com/img/bVbjYU7?w=2000&h=1333); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! JavsScript 是一門單線程的編程語言,這就意味著一個時間里只能處理一件事,也就是說 JavaScript 引擎一次只能在一個線程里處理一條語句。 雖然單線程簡化了編程代碼,因為你不必太擔心并發引出的問...
閱讀 1883·2021-11-22 09:34
閱讀 3010·2021-09-28 09:35
閱讀 13374·2021-09-09 11:34
閱讀 3594·2019-08-29 16:25
閱讀 2820·2019-08-29 15:23
閱讀 2035·2019-08-28 17:55
閱讀 2424·2019-08-26 17:04
閱讀 3044·2019-08-26 12:21