異步的JavaScript

panel 6年前發布 | 30K 次閱讀 JavaScript開發 JavaScript

引子

前幾天學校的交流群里面討論JavaScript回調函數,有個同學提出了一個觀點:回調函數就是異步執行的!

看到這個觀點,我想了想我使用回調函數的場景,還真都是異步的,一時竟覺得他說得很有道理。

當然,這句話本身,當然是 錯的 ,在JavaScript中函數作為一等公民,可以在任何地方定義,在函數內或函數外,可以作為函數的參數和返回值,基于這個基本事實,就可以寫出高階函數。

接受或者返回一個函數的函數稱為高階函數

常用的內置高階函數例如Array對象的forEach,map等, 函數組合函數柯里化 ,回調模式都屬于高階函數。實際上,JavaScript是有能力進行函數式編程的( FED關于函數式編程的文章 ),這里就不深入討論了。

有點扯遠了,回到剛剛那個同學觀點,和明顯,用forEach遍歷一下數組,并不是異步的,那么為什么提到回調函數的時候,很容易和異步聯系起來呢?這個問題引起了我的思考,回調和異步到底有什么樣的淵源呢?

單線程的JavaScript

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

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

為什么需要異步?

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

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

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

JavaScript執行機制

在談異步之前,先來說說JavaScript的執行機制,看下面這段代碼

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

// Uncaught RangeError: Maximum call stack size exceeded</pre>

這代碼里面拋出了一個錯誤,意思是超過最大調用堆棧大小,那么這個call stack是什么呢?

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

stack 是一種數據結構,數據先入后出,后入先出。執行JavaScript的call 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</pre>

因為setTimeout的函數經過webApi,0ms后定時器執行并將回調函數放到task queue,當call stack中的代碼執行完畢時,主進程不斷查看task queue中的任務,如果有任務就取出并放到call stack中執行。

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

對于其他的異步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中的代碼就會一直處于等待狀態,此時瀏覽器無法進行任何交互和操作,頁面就相當于掛掉了。

nodeJS中的異步

node作為一個事件驅動,無阻塞IO的運行時,也是使用了事件循環。但是node和瀏覽器的實現有很多不同,下次可以再水一篇node文章(逃

 

來自:https://webfe.kujiale.com/yi-bu-de-javascript/

 

 本文由用戶 panel 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!