詳細解析JavaScript中的非同步機制
學習JavaScript的時候瞭解到JavaScript是單執行緒的,剛開始很疑惑,單執行緒怎麼處理網路請求、檔案讀寫等耗時操作呢?效率豈不是會很低?隨著對這方面內容的瞭解和深入,知道了其中的奧祕。本篇文章就主要講解一下JavaScript怎麼處理非同步問題。
一、同步與非同步
在介紹JavaScript的非同步機制之前,首先介紹一下:什麼是同步?什麼是非同步?

同步
如果在函式返回的時候,呼叫者就能夠得到預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函式就是同步的。
如下所示:
//在函式返回時,獲得了預期值,即2的平方根 Math.sqrt(2); //在函式返回時,獲得了預期的效果,即在控制檯上列印了'hello' console.log('hello');
上面兩個函式就是同步的。
如果函式是同步的,即使呼叫函式執行的任務比較耗時,也會一直等待直到得到預期結果。
非同步
如果在函式返回的時候,呼叫者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那麼這個函式就是非同步的。
如下所示:
//讀取檔案 fs.readFile('hello.txt', 'utf8', function(err, data) { console.log(data); }); //網路請求 var xhr = new XMLHttpRequest(); xhr.onreadystatechange = xxx; // 添加回調函式 xhr.open('GET', url); xhr.send(); // 發起函式
上述示例中讀取檔案函式 readFile
和網路請求的發起函式 send
都將執行耗時操作,雖然函式會立即返回,但是不能立刻獲取預期的結果,因為耗時操作交給其他執行緒執行,暫時獲取不到預期結果(後面介紹)。而在JavaScript中通過回撥函式 function(err, data) { console.log(data); }
和 onreadystatechange
,在耗時操作執行完成後把相應的結果資訊傳遞給回撥函式,通知執行JavaScript程式碼的執行緒執行回撥。
如果函式是非同步的,發出呼叫之後,馬上返回,但是不會馬上返回預期結果。呼叫者不必主動等待,當被呼叫者得到結果之後會通過回撥函式主動通知呼叫者。
二、單執行緒與多執行緒

在上面介紹非同步的過程中就可能會納悶:既然JavaScript是單執行緒,怎麼還存在非同步,那些耗時操作到底交給誰去執行了?
JavaScript其實就是一門語言,說是單執行緒還是多執行緒得結合具體執行環境。JS的執行通常是在瀏覽器中進行的,具體由JS引擎去解析和執行。下面我們來具體瞭解一下瀏覽器。
瀏覽器
目前最為流行的瀏覽器為:Chrome,IE,Safari,FireFox,Opera。瀏覽器的核心是多執行緒的。
一個瀏覽器通常由以下幾個常駐的執行緒:
- 渲染引擎執行緒:顧名思義,該執行緒負責頁面的渲染
- JS引擎執行緒:負責JS的解析和執行
- 定時觸發器執行緒:處理定時事件,比如setTimeout, setInterval
- 事件觸發執行緒:處理DOM事件
- 非同步http請求執行緒:處理http請求
需要注意的是,渲染執行緒和JS引擎執行緒是不能同時進行的。渲染執行緒在執行任務的時候,JS引擎執行緒會被掛起。因為JS可以操作DOM,若在渲染中JS處理了DOM,瀏覽器可能就不知所措了。
JS引擎
通常講到瀏覽器的時候,我們會說到兩個引擎:渲染引擎和JS引擎。渲染引擎就是如何渲染頁面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎對同一個樣式的實現不一致,就導致了經常被人詬病的瀏覽器樣式相容性問題。這裡我們不做具體討論。
JS引擎可以說是JS虛擬機器,負責JS程式碼的解析和執行。通常包括以下幾個步驟:
- 詞法分析:將原始碼分解為有意義的分詞
- 語法分析:用語法分析器將分詞解析成語法樹
- 程式碼生成:生成機器能執行的程式碼
- 程式碼執行
不同瀏覽器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。
之所以說JavaScript是單執行緒,就是因為瀏覽器在執行時只開啟了一個JS引擎執行緒來解析和執行JS。那為什麼只有一個引擎呢?如果同時有兩個執行緒去操作DOM,瀏覽器是不是又要不知所措了。
所以,雖然JavaScript是單執行緒的,可是瀏覽器內部不是單執行緒的。一些I/O操作、定時器的計時和事件監聽(click, keydown...)等都是由瀏覽器提供的其他執行緒來完成的。
三、訊息佇列與事件迴圈
通過以上了解,可以知道其實JavaScript也是通過JS引擎執行緒與瀏覽器中其他執行緒互動協作實現非同步。但是回撥函式具體何時加入到JS引擎執行緒中執行?執行順序是怎麼樣的?
這一切的解釋就需要繼續瞭解訊息佇列和事件迴圈。

如上圖所示,左邊的棧儲存的是同步任務,就是那些能立即執行、不耗時的任務,如變數和函式的初始化、事件的繫結等等那些不需要回調函式的操作都可歸為這一類。
右邊的堆用來儲存宣告的變數、物件。下面的佇列就是訊息佇列,一旦某個非同步任務有了響應就會被推入佇列中。如使用者的點選事件、瀏覽器收到服務的響應和setTimeout中待執行的事件,每個非同步任務都和回撥函式相關聯。
JS引擎執行緒用來執行棧中的同步任務,當所有同步任務執行完畢後,棧被清空,然後讀取訊息佇列中的一個待處理任務,並把相關回調函式壓入棧中,單執行緒開始執行新的同步任務。
JS引擎執行緒從訊息佇列中讀取任務是不斷迴圈的,每次棧被清空後,都會在訊息佇列中讀取新的任務,如果沒有新的任務,就會等待,直到有新的任務,這就叫事件迴圈。

上圖以AJAX非同步請求為例,發起非同步任務後,由AJAX執行緒執行耗時的非同步操作,而JS引擎執行緒繼續執行堆中的其他同步任務,直到堆中的所有非同步任務執行完畢。然後,從訊息佇列中依次按照順序取出訊息作為一個同步任務在JS引擎執行緒中執行,那麼AJAX的回撥函式就會在某一時刻被呼叫執行。
四、示例
引用一篇文章中提到的考察JavaScript非同步機制的面試題來具體介紹。
執行下面這段程式碼,執行後,在 5s 內點選兩下,過一段時間(>5s)後,再點選兩下,整個過程的輸出結果是什麼?
setTimeout(function(){ for(var i = 0; i < 100000000; i++){} console.log('timer a'); }, 0) for(var j = 0; j < 5; j++){ console.log(j); } setTimeout(function(){ console.log('timer b'); }, 0) function waitFiveSeconds(){ var now = (new Date()).getTime(); while(((new Date()).getTime() - now) < 5000){} console.log('finished waiting'); } document.addEventListener('click', function(){ console.log('click'); }) console.log('click begin'); waitFiveSeconds();
要想了解上述程式碼的輸出結果,首先介紹下定時器。
setTimeout
的作用是在間隔一定的時間後,將回調函式插入訊息佇列中,等棧中的同步任務都執行完畢後,再執行。因為棧中的同步任務也會耗時, 所以間隔的時間一般會大於等於指定的時間 。
setTimeout(fn, 0)
的意思是,將回調函式fn立刻插入訊息佇列,等待執行,而不是立即執行。看一個例子:
setTimeout(function() { console.log("a") }, 0) for(let i=0; i<10000; i++) {} console.log("b")
ba
列印結果表明回撥函式並沒有立刻執行,而是等待棧中的任務執行完畢後才執行的。棧中的任務執行多久,它就得等多久。
理解了定時器的作用,那麼對於輸出結果就容易得出了。
首先,先執行同步任務。其中 waitFiveSeconds
是耗時操作,持續執行長達5s。
0 1 2 3 4 click begin finished waiting
然後,在JS引擎執行緒執行的時候,'timer a'對應的定時器產生的回撥、 'timer b'對應的定時器產生的回撥和兩次 click 對應的回撥被先後放入訊息佇列。由於JS引擎執行緒空閒後,會 先檢視是否有事件可執行 ,接著再處理其他非同步任務。因此會產生 下面的輸出順序。
click click timer a timer b
最後,5s 後的兩次 click 事件被放入訊息佇列,由於此時JS引擎執行緒空閒,便被立即執行了。
click click
自己是從事了七年開發的Android工程師,不少人私下問我,2019年Android進階該怎麼學,方法有沒有?
沒錯,年初我花了一個多月的時間整理出來的學習資料,希望能幫助那些想進階提升Android開發,卻又不知道怎麼進階學習的朋友。【 包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全