之前看的文,感覺不太完整,於是找了篇詳細的文章原文連結。翻譯不正確的請指出,重在分享,如果有所收穫就更好了。
Javascript是如何非同步和單執行緒的?簡短的回答是javascript語言是單執行緒的,非同步行為不是它的一部分,相反,它是建立在瀏覽器(或程式設計環境)中的核心JavaScript語言之上,並通過瀏覽器API訪問。
現在為了得到答案,讓我寫兩個示例程式碼片段。
基本架構
- 堆:物件在堆中分配,表示大多數非結構化的記憶體區域。
- 棧:這表示JavaScript程式碼執行提供的單個執行緒,函式呼叫形成一個棧。
- 瀏覽器或Web API是內建在你的web瀏覽器中,能夠暴露瀏覽器和周圍的計算機環境中的資料,並使用它執行遊泳的複雜操作。它們不是JavaScript語言的一部分,而是基於核心JavaScript語言構建,為你的JavaScript程式碼中提供額外的超能力。例如,Geolocation API提供了一些簡單的JavaScript構造,用於檢索資料,所以你可以說,在
Google map
上繪製你的位置。在後臺,瀏覽器實際上使用一些複雜的低階程式碼(例如C++)與裝置的GPS硬體(或者任何可用於確定位置資料的資訊)進行通訊,檢索位置資料,並將其返回到瀏覽器環境用來使用在你的程式碼中。但同樣,這種複雜性是由API抽象出來的。
程式碼片段1:迷惑心靈
function main(){
console.log('A');
setTimeout(
function display(){ console.log('B'); }
,0);
console.log('C');
}
main();
// Output
// A
// C
// B
複製程式碼
這裡,我們有一個主函式,它有兩個console.log
:將A和C輸入到控制檯上。它們中間是一個在0ms後將B輸出到控制檯上的setTimeOut
呼叫。
- 呼叫主函式,首先將它推入到棧中。然後瀏覽器將主函式的第一個宣告
console.log('A')
推入到棧中,執行此語句,並在完成後彈出,字母A顯示在控制檯上。 - 第二個宣告
setTimeout
推入棧中並開始執行。setTimeout
函式使用瀏覽器的API來延遲迴調其中的函式。一旦交接給brower完成timer,這一幀就被推出。 - 當計時器在瀏覽器中執行來回撥
exec
函式時,console.log(‘C’)
被推入棧中。在這種特殊的情況下,由於提供的延遲是0ms,一旦瀏覽器接收到它(理想情況下),回撥將被新增到訊息佇列中。 - 執行完主函式的最後一個語句,主函式從呼叫堆疊中彈出,從而堆疊為空。對於瀏覽器將任何訊息從佇列推送到呼叫堆疊中,呼叫堆疊必須為空。這就是為什麼即使
setTimeout
中提供的延遲是0ms,exec
的也必須等到呼叫堆疊中的所有幀的執行完成。 - 現在
exec
的回撥被推入呼叫棧,接著執行。字母C顯示在控制檯上。這就是JavaScript的事件迴圈。
因此setTimeout(function,delayTime)中的delay引數不代表執行函式之後的精確時間延遲。它代表最小等待時間,之後在某個時間點執行該功能。
程式碼片段2:深入理解
function main(){
console.log('A');
setTimeout(
function exec(){ console.log('B'); }
, 0);
runWhileLoopForNSeconds(3);
console.log('C');
}
main();
function runWhileLoopForNSeconds(sec){
let start = Date.now(), now = start;
while (now - start < (sec*1000)) {
now = Date.now();
}
}
// Output
// A
// C
// B
複製程式碼
- 函式
runWhileLoopForNSeconds
完全符合其名稱所代表的含義,它會不斷檢查從呼叫時間開始經過的時間是否等於函式引數提供的秒數。要記住的要點是while
迴圈(與許多其他迴圈一樣)是一個阻塞語句,意味著它的執行發生在呼叫堆疊上,並且不使用瀏覽器API。因此它會阻止所有後續語句,直到它執行完成。 - 所以在上面的程式碼中,即使setTimeout的延遲為0ms,但在while迴圈執行3s中,exec()回撥會卡在訊息佇列中。只有在while迴圈在呼叫堆疊(單執行緒)上執行3s完成,呼叫堆疊變空之後,回撥exec()被推入到呼叫堆疊並執行。
- 因此
setTimeout()
中的delay引數不保證在計時器完成延遲後開始執行。它是延遲部分的最短時間。