異步的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中的異步任務,現在先限定在瀏覽器中,可以得出以下結果:
dom事件
定時器setTimeout,setInterval
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/