非同步/回撥

weixin_33782386發表於2018-06-13

單執行緒的JavaScript

說起非同步,就要先說說JavaScript執行機制。我們知道,JavaScript是單執行緒執行的,意味著同一個時間點,只有一個任務在執行。單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

從誕生起,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。

為什麼需要非同步?

單執行緒的好處是實現起來比較簡單,執行環境相對單純;壞處是隻要有一個任務耗時很長,後面的任務都必須排隊等著,會拖延整個程式的執行。常見的瀏覽器無響應(假死),往往就是因為某一段Javascript程式碼長時間執行(比如死迴圈),導致整個頁面卡在這個地方,其他任務無法執行。

為了解決這個問題,Javascript語言將任務的執行模式分成兩種:同步(Synchronous)和非同步(Asynchronous)。

關於非同步處理,現在已經有了許多好的解決方案。如果想全面瞭解如何處理非同步,@肖雞已經寫了一篇非常全面的文章JS中非同步的解決方案

JavaScript執行機制

在談非同步之前,先來說說JavaScript的執行機制,看下面這段程式碼

function foo () {
    return foo();
}
foo();

// Uncaught RangeError: Maximum call stack size exceeded

這程式碼裡面丟擲了一個錯誤,意思是超過最大呼叫堆疊大小,那麼這個call stack是什麼呢?

call stack:執行JavaScript的主執行緒分為heap和stack,stack是一個執行環境上下文。

stack是一種資料結構,資料先入後出,後入先出。執行JavaScript的call stack,也是如此。

7981386-8656fe2819f45879.png
stack

從上圖的例子可以看出呼叫棧的變化

main(js檔案可以視作一個main函式) -> printSquare(內部呼叫了square,因此需要把square推入棧中) -> square(內部呼叫了multiply,推入棧中) -> multiply

此時所依賴的函式都在棧中,那麼可以執行了,執行順序和棧是一致的,後入先出(執行),所以順序為
multiply -> square -> printSquare。

call stack也是有最大限制的,可以使用下面的程式碼測試一下瀏覽器的最大call stack size


var i = 0;
function inc() {
    i++;
    inc();
}
inc();
//VM202:2 Uncaught RangeError: Maximum call stack size exceeded
i // 15720

理解JavaScript的函式呼叫方式,對於理解遞迴,高階函式,非同步函式等都是非常有幫助的。

以遞迴為例,遞迴函式不斷呼叫自身,那麼就會不斷向call stack中推入函式,直到達到遞迴條件(此時函式不再呼叫自身),然後再按後進先出的原則依次執行stack中的函式。

非同步的實現

非同步的實現我分為三部分來理解:webApi,任務佇列,event loop

webApi

先來列舉一下JavaScript中的非同步任務,現在先限定在瀏覽器中,可以得出以下結果:

  1. dom事件

  2. 定時器setTimeout,setInterval

  3. XMLHttpRequest

可以發現,這些都是都是瀏覽器的一些api,也就是webApi。其實非同步的實現是瀏覽器來處理的,主執行緒並不用管非同步時如何實現的。

事實上,瀏覽器是多程式的,所以可以開多個執行緒來處理非同步行為,並在任務完成時同步到任務佇列

任務佇列

看下面這段程式碼,setTimeout指定的函式0ms後輸出,但是最後才執行

console.log(1);
setTimeout(() => {console.log('after 0ms')} ,0);
console.log(2);
console.log(3);

// 1
// 2
// 3
// after 0ms

因為setTimeout的函式經過webApi,0ms後定時器執行並將回撥函式放到task queue,當call stack中的程式碼執行完畢時,主程式不斷檢視task queue中的任務,如果有任務就取出並放到call stack中執行。

setTimeout的定時是不準確的,因為當前call stack執行任務時,定時器的回撥就會一直在task queue中等待

7981386-d0feac4b2beeccac.png
event loop

對於其他的非同步api,如dom事件,ajax請求等,都是同樣的原理,當非同步事件執行完畢,就會把相應的回撥函式放到task queue中。

task queue中的任務需要反覆輪詢,檢視是否有任務已完成,這個輪詢就是event loop

Event loop

event loop經常用類似如下的方式來實現


while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果當前沒有任何訊息queue.waitForMessage 會等待同步訊息到達。

非同步和回撥的關係

說到現在,非同步和回撥的關係已經很明確了。

非同步:通過webApi建立非同步任務。任務完成時,如果有指定了回撥函式,將回撥函式放入task queue中;如果沒有指定回撥函式,這個事件就被丟棄。

回撥函式:定義了非同步任務完成時所要執行的操作,包括事件和定時器所指定的非同步任務。

避免同步阻塞的程式碼

像深度迴圈,同步的ajax請求等任務會非常耗時,主執行緒有程式碼執行時,task queue中的程式碼就會一直處於等待狀態,此時瀏覽器無法進行任何互動和操作,頁面就相當於掛掉了。

相關文章