您當前的位置:首頁 > 美食

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

作者:由 前端小智 發表于 美食時間:2019-07-15

原文:

https://www。

valentinog。com/blog/eng

ines/?utm_source=mybridge&utm_medium=blog&utm_campaign=read_more

譯者:前端小智

為了保證可讀性,本文采用意譯而非直譯。

想閱讀更多優質文章請猛戳GitHub部落格,一年百來篇優質文章等著你!

有沒有想過瀏覽器如何讀取和執行JS程式碼? 這看起來很神奇,我們可以透過瀏覽器提供的控制檯來了解背後的一些原理。

在Chrome中開啟瀏覽器控制檯,然後檢視

Sources

這欄,在右側可以到一個

Call Stack

盒子。

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

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引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

即使示例只有變數和函式,也要考慮你的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引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

同時,JS 引擎還分配了一個

全域性執行上下文

,這是執行JS程式碼的全域性環境,如下所示

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

想象全域性執行上下文是一個海洋,其中全域性函式像魚一樣遊動,多美好! 但現實遠非那麼簡單, 如果我函式有一些巢狀變數或一個或多個內部函式怎麼辦?

即使是像下面這樣的簡單變化,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!’);

}

示意圖如下:

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

如你所見,

setTimeout

在瀏覽器上下文中執行。 10秒後,計時器被觸發,回撥函式準備執行。 但首先它必須透過

回撥佇列(Callback Queue

)。 回撥佇列是一個佇列資料結構,回撥佇列是一個有序的函式佇列。

每個非同步函式在被放入呼叫堆疊之前必須透過回撥佇列

,但這個工作是誰做的呢,那就是

事件迴圈

(Event Loop)。

事件迴圈只有一個任務:它檢查呼叫堆疊是否為空

。如果回

調佇列中

(Callback Queue)有某個函式,並且呼叫堆疊是空閒的,那麼就將其放入呼叫堆疊中。

完成後,執行該函式。 以下是用於處理非同步和同步程式碼的JS引擎的圖:

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

想象一下,

callback()

已準備好執行,當

pow()

完成時,

呼叫堆疊

(Call Stack) 為空,

事件迴圈

(Event Look) 將

callback()

放入呼叫堆中。大概就是這樣,如果你理解了上面的插圖,那麼你就可以理解所有的JavaScript了。

回撥地獄和 ES6 中的Promises

JS 中回撥函式無處不在,它們用於同步和非同步程式碼。 考慮如下

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/await

ECMAScript 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”));

執行結果:

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

以上兩個字串都會列印。 請記住,

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中構建非同步程式碼的最佳方式

。 我們可以更好地控制錯誤處理,程式碼看起來也更清晰。

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。

總結

JS 是一種用於Web的指令碼語言,具有先編譯然後由引擎解釋的特性。 在最流行的JS引擎中,有谷歌Chrome和Node。js使用的V8,有Firefox構建的

SpiderMonkey

,以及Safari使用的

JavaScriptCore

JS引擎包含很有元件:呼叫堆疊、全域性記憶體(堆)、事件迴圈、回撥佇列。所有這些元件一起工作,完美地進行了調優,以處理JS中的同步和非同步程式碼。

JS引擎是單執行緒的

,這意味著執行函式只有一個呼叫堆疊。這一限制是JS非同步本質的基礎:所有需要時間的操作都必須由外部實體(例如瀏覽器)或回撥函式負責。

為了簡化非同步程式碼流,

ECMAScript 2015 給我們帶來了Promise

。 Promise 是一個非同步物件,用於表示任何非同步操作的失敗或成功。 但改進並沒有止步於此。

在2017年,async/ await誕生了

:它是

Promise

的一種風格彌補,使得編寫非同步程式碼成為可能,就好像它是同步的一樣。

交流

乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。

https://

github。com/qq449245884/

xiaozhi

我是小智,公眾號「大遷世界」作者,

對前端技術保持學習愛好者。我會經常分享自己所學所看的乾貨

,在進階的路上,共勉!

關注公眾號,後臺回覆

福利

,即可看到福利,你懂的。

JS引擎:它們是如何工作的?從呼叫堆疊到Promise,需要知道的所有內容

標簽: promise  js  函式  function  非同步