摘要:最受歡迎的引擎是,由和使用,用于,以及使用的。引擎它們是如何工作的全局執行上下文和調用堆棧剛剛了解了引擎如何讀取變量和函數聲明,它們最終被放入了全局內存堆中。事件循環只有一個任務它檢查調用堆棧是否為空。
為了保證可讀性,本文采用意譯而非直譯。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
有沒有想過瀏覽器如何讀取和運行JS代碼? 這看起來很神奇,我們可以通過瀏覽器提供的控制臺來了解背后的一些原理。
在Chrome中打開瀏覽器控制臺,然后查看Sources這欄,在右側可以到一個 Call Stack 盒子。
JS 引擎是一個可以編譯和解釋我們的JS代碼強大的組件。 最受歡迎的JS 引擎是V8,由 Google Chrome 和 Node.j s使用,SpiderMonkey 用于Firefox,以及Safari/WebKit使用的 JavaScriptCore。
雖然現在 JS 引擎不是幫我們處理全面的工作。但是每個引擎中都有一些較小的組件為我們做繁瑣的的工作。
其中一個組件是調用堆棧(Call Stack),與全局內存和執行上下文一起運行我們的代碼。
Js 引擎和全局內存(Global Memory)JavaScript 是編譯語言同時也是解釋語言。信不信由你,JS 引擎在執行代碼之前只需要幾微秒就能編譯代碼。
這聽起來很神奇,對吧?這種神奇的功能稱為JIT(及時編譯)。這個是一個很大的話題,一本書都不足以描述JIT是如何工作的。但現在,我們午飯可以跳過編譯背后的理論,將重點放在執行階段,盡管如此,這仍然很有趣。
考慮以下代碼:
var num = 2; function pow(num) { return num * num; }
如果問你如何在瀏覽器中處理上述代碼? 你會說些什么? 你可能會說“瀏覽器讀取代碼”或“瀏覽器執行代碼”。
現實比這更微妙。首先,讀取這段代碼的不是瀏覽器,是JS引擎。JS引擎讀取代碼,一旦遇到第一行,就會將幾個引用放入全局內存。
全局內存(也稱為堆)JS引擎保存變量和函數聲明的地方。因此,回到上面示例,當 JS引擎讀取上面的代碼時,全局內存中放入了兩個綁定。
即使示例只有變量和函數,也要考慮你的JS代碼在更大的環境中運行:在瀏覽器中或在Node.js中。 在這些環境中,有許多預定義的函數和變量,稱為全局變量。 全球記憶將比num和pow更多。
上例中,沒有執行任何操作,但是如果我們像這樣運行函數會怎么樣呢:
var num = 2; function pow(num) { return num * num; } pow(num);
現在事情變得有趣了。當函數被調用時,JavaScript引擎會為全局執行上下文和調用棧騰出空間。
JS引擎:它們是如何工作的? 全局執行上下文和調用堆棧剛剛了解了 JS引擎如何讀取變量和函數聲明,它們最終被放入了全局內存(堆)中。
但現在我們執行了一個JS函數,JS引擎必須處理它。怎么做?每個JS引擎中都有一個基本組件,叫調用堆棧。
調用堆棧是一個堆棧數據結構:這意味著元素可以從頂部進入,但如果它們上面有一些元素,它們就不能離開,JS 函數就是這樣的。
一旦執行,如果其他函數仍然被阻塞,它們就不能離開調用堆棧。請注意,這個有助于你理解“JavaScript是單線程的”這句話。
回到我們的例子,當函數被調用時,JS引擎將該函數推入調用堆棧
同時,JS 引擎還分配了一個全局執行上下文,這是運行JS代碼的全局環境,如下所示
想象全局執行上下文是一個海洋,其中全局函數像魚一樣游動,多美好! 但現實遠非那么簡單, 如果我函數有一些嵌套變量或一個或多個內部函數怎么辦?
即使是像下面這樣的簡單變化,JS引擎也會創建一個本地執行上下文:
var num = 2; function pow(num) { var fixed = 89; return num * num; } pow(num);
注意,我在pow函數中添加了一個名為fixed的變量。在這種情況下,pow函數中會創建一個本地執行上下文,fixed 變量被放入pow函數中的本地執行上下文中。
對于嵌套函數的每個嵌套函數,引擎都會創建更多的本地執行上下文。
JavaScript 是單線程和其他有趣的故事JavaScript是單線程的,因為只有一個調用堆棧處理我們的函數。也就是說,如果有其他函數等待執行,函數就不能離開調用堆棧。
在處理同步代碼時,這不是問題。例如,兩個數字之間的和是同步的,以微秒為單位。但如果涉及異步的時候,怎么辦呢?
幸運的是,默認情況下JS引擎是異步的。即使它一次執行一個函數,也有一種方法可以讓外部(如:瀏覽器)執行速度較慢的函數,稍后探討這個主題。
當瀏覽器加載某些JS代碼時,JS引擎會逐行讀取并執行以下步驟:
將變量和函數的聲明放入全局內存(堆)中
將函數的調用放入調用堆棧
創建全局執行上下文,在其中執行全局函數
創建多個本地執行上下文(如果有內部變量或嵌套函數)
到目前為止,對JS引擎的同步機制有了基本的了解。 在接下來的部分中,講講 JS 異步工作原理。
異步JS,回調隊列和事件循環全局內存(堆),執行上下文和調用堆棧解釋了同步 JS 代碼在瀏覽器中的運行方式。 然而,我們遺漏了一些東西,當有一些異步函數運行時會發生什么?
請記住,調用堆棧一次可以執行一個函數,甚至一個阻塞函數也可以直接凍結瀏覽器。 幸運的是JavaScript引擎是聰明的,并且在瀏覽器的幫助下可以解決問題。
當我們運行一個異步函數時,瀏覽器接受該函數并運行它??紤]如下代碼:
setTimeout(callback, 10000); function callback(){ console.log("hello timer!"); }
setTimeout 大家都知道得用得很多次了,但你可能不知道它不是內置的JS函數。 也就是說,當JS 出現,語言中沒有內置的setTimeout。
setTimeout瀏覽器API( Browser API)的一部分,它是瀏覽器免費提供給我們的一組方便的工具。這在實戰中意味著什么?由于setTimeout是一個瀏覽器的一個Api,函數由瀏覽器直接運行(它會在調用堆棧中出現一會兒,但會立即刪除)。
10秒后,瀏覽器接受我們傳入的回調函數并將其移動到回調隊列(Callback Queu)中。??紤]以下代碼
var num = 2; function pow(num) { return num * num; } pow(num); setTimeout(callback, 10000); function callback(){ console.log("hello timer!"); }
示意圖如下:
如你所見,setTimeout在瀏覽器上下文中運行。 10秒后,計時器被觸發,回調函數準備運行。 但首先它必須通過回調隊列(Callback Queue)。 回調隊列是一個隊列數據結構,回調隊列是一個有序的函數隊列。
每個異步函數在被放入調用堆棧之前必須通過回調隊列,但這個工作是誰做的呢,那就是事件循環(Event Loop)。
事件循環只有一個任務:它檢查調用堆棧是否為空。如果回調隊列中(Callback Queue)有某個函數,并且調用堆棧是空閑的,那么就將其放入調用堆棧中。
完成后,執行該函數。 以下是用于處理異步和同步代碼的JS引擎的圖:
想象一下,callback() 已準備好執行,當 pow() 完成時,調用堆棧(Call Stack) 為空,事件循環(Event Look) 將 callback() 放入調用堆中。大概就是這樣,如果你理解了上面的插圖,那么你就可以理解所有的JavaScript了。
回調地獄和 ES6 中的PromisesJS 中回調函數無處不在,它們用于同步和異步代碼。 考慮如下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?;卣{在JS中無處不在,因此就會出現了一個問題:回調地獄。
JavaScript 中的回調地獄指的是一種編程風格,其中回調嵌套在回調函數中,而回調函數又嵌套在其他回調函數中。由于 JS 異步特性,js 程序員多年來陷入了這個陷阱。
說實話,我從來沒有遇到過極端的回調金字塔,這可能是因為我重視可讀代碼,而且我總是堅持這個原則。如果你在遇到了回調地獄的問題,說明你的函數做得太多。
這里不會討論回調地獄,如果你好奇,有一個網站,callbackhell.com,它更詳細地探索了這個問題,并提供了一些解決方案。
我們現在要關注的是ES6的 Promises。ES6 Promises是JS語言的一個補充,旨在解決可怕的回調地獄。但什么是 Promises 呢?
JS的 Promise是未來事件的表示。 Promise 可以以成功結束:用行話說我們已經解決了resolved(fulfilled)。 但如果 Promise 出錯,我們會說它處于拒絕(rejected )狀態。 Promise 也有一個默認狀態:每個新的 Promise 都以掛起(pending)狀態開始。
創建和使用 JavaScript 的 Promises要創建一個新的 Promise,可以通過傳遞回調函數來調用 Promise 構造函數。回調函數可以接受兩個參數:resolve和reject。如下所示:
const myPromise = new Promise(function(resolve){ setTimeout(function(){ resolve() }, 5000) });
如下所示,resolve是一個函數,調用它是為了使Promise 成功,別外也可以使用 reject 來表示調用失敗。
const myPromise = new Promise(function(resolve, reject){ setTimeout(function(){ reject() }, 5000) });
注意,在第一個示例中可以省略reject,因為它是第二個參數。但是,如果打算使用reject,則不能忽略resolve,如下所示,最終將得到一個resolved 的承諾,而非 reject。
// 不能忽略 resolve ! const myPromise = new Promise(function(reject){ setTimeout(function(){ reject() }, 5000) });
現在,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); });Promises 的錯誤處理
對于同步代碼而言,JS 錯誤處理大都很簡單,如下所示:
function makeAnError() { throw Error("Sorry mate!"); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); }
將會輸出:
Catching the error! Error: Sorry mate!
現在嘗試使用異步函數:
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()來創建和拒絕一個Promise
Promise.reject({msg: "Rejected!"}).catch(err => console.log(err));Promises 組合:Promise.all,Promise.allSettled, Promise.any
Promise API 提供了許多將Promise組合在一起的方法。 其中最有用的是Promise.all,它接受一個Promises數組并返回一個Promise。 如果參數中 promise 有一個失敗(rejected),此實例回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。
Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。
較新版本的V8也將實現兩個新的組合:Promise.allSettled和Promise.any。 Promise.any仍然處于提案的早期階段:在撰寫本文時,仍然沒有瀏覽器支持它。
Promise.any可以表明任何Promise是否fullfilled。 與 Promise.race的區別在于Promise.any不會拒絕即使其中一個Promise被拒絕。
無論如何,兩者中最有趣的是 Promise.allSettled,它也是 Promise 數組,但如果其中一個Promise拒絕,它不會短路。 當你想要檢查Promise數組是否全部已解決時,它是有用的,無論最終是否拒絕,可以把它想象成Promise.all 的反對者。
異步進化:從Promises 到 async/awaitECMAScript 2017 (ES8)的出現,推出了新的語法誕生了async/await。
async/await只是Promise 語法糖。它只是一種基于Promises編寫異步代碼的新方法, async/await 不會以任何方式改變JS,請記住,JS必須向后兼容舊瀏覽器,不應破壞現有代碼。
來個例子:
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();
有趣的是,async 函數也會返回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函數,我們可以重構以上代碼:
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時,還有另一個怪異的地方需要指出,如下所示:
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觸then的處理程序。 一種的解決方案是從函數返回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" // 輸出
除此之外,async/await似乎是在JS中構建異步代碼的最佳方式。 我們可以更好地控制錯誤處理,代碼看起來也更清晰。
總結JS 是一種用于Web的腳本語言,具有先編譯然后由引擎解釋的特性。 在最流行的JS引擎中,有谷歌Chrome和Node.js使用的V8,有Firefox構建的SpiderMonkey,以及Safari使用的JavaScriptCore。
JS引擎包含很有組件:調用堆棧、全局內存(堆)、事件循環、回調隊列。所有這些組件一起工作,完美地進行了調優,以處理JS中的同步和異步代碼。
JS引擎是單線程的,這意味著運行函數只有一個調用堆棧。這一限制是JS異步本質的基礎:所有需要時間的操作都必須由外部實體(例如瀏覽器)或回調函數負責。
為了簡化異步代碼流,ECMAScript 2015 給我們帶來了Promise。 Promise 是一個異步對象,用于表示任何異步操作的失敗或成功。 但改進并沒有止步于此。 在2017年,async/ await誕生了:它是Promise的一種風格彌補,使得編寫異步代碼成為可能,就好像它是同步的一樣。
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105180.html
摘要:事件循環從回調隊列中獲取并將其推入調用堆棧。執行從調用堆棧中移除從調用堆棧中移除快速回顧值得注意的是,指定了事件循環應該如何工作,這意味著在技術上它屬于引擎的職責范圍,不再僅僅扮演宿主環境的角色。 此篇是 JavaScript是如何工作的第四篇,其它三篇可以看這里: JavaScript是如何工作的:引擎,運行時和調用堆棧的概述! JavaScript是如何工作的:深入V8引擎&編寫...
摘要:事件循環從回調隊列中獲取并將其推送到調用堆棧。如何工作請注意,不會自動將您的回調函數放到事件循環隊列中。它設置了一個計時器,當計時器到期時,環境將您的回調函數放入事件循環中,以便將來的某個事件會將其選中并執行它。 我們將通過回顧第一篇文章中單線程編程的缺點,然后在討論如何克服它們來構建令人驚嘆的JavaScript UI。在文章結尾處,我們將分享5個關于如何使用async / awai...
摘要:最受歡迎的引擎是,在和中使用,用于,以及所使用的。怎么處理每個引擎都有一個基本組件,稱為調用棧。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。每個異步函數在被送入調用棧之前必須通過回調隊列。例如方法是在中傳遞的回調函數。 ? 翻譯:瘋狂的技術宅 原文:www.valentinog.com/blog/engine… 從Call Stack,Global Me...
摘要:最受歡迎的引擎是,在和中使用,用于,以及所使用的。單線程的我們說是單線程的,因為有一個調用棧處理我們的函數。也就是說,如果有其他函數等待執行,函數是不能離開調用棧的。每個異步函數在被送入調用棧之前必須通過回調隊列。 翻譯:瘋狂的技術宅原文:https://www.valentinog.com/bl... 本文首發微信公眾號:前端先鋒歡迎關注,每天都給你推送新鮮的前端技術文章 sh...
showImg(https://segmentfault.com/img/bVbjYU7?w=2000&h=1333); 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! JavsScript 是一門單線程的編程語言,這就意味著一個時間里只能處理一件事,也就是說 JavaScript 引擎一次只能在一個線程里處理一條語句。 雖然單線程簡化了編程代碼,因為你不必太擔心并發引出的問...
閱讀 1378·2021-09-24 10:26
閱讀 1689·2019-08-30 14:14
閱讀 2098·2019-08-29 16:54
閱讀 363·2019-08-29 14:09
閱讀 1468·2019-08-29 12:55
閱讀 922·2019-08-28 18:13
閱讀 1573·2019-08-26 13:39
閱讀 2560·2019-08-26 11:43