JavaScript異步編程
還記得一年前寫過一篇關于 JavaScript異步編程簡述 的文章,主要介紹了JavaScript的單線程特性與異步編程實現方式:
回調函數,發布訂閱模式,Promise對象三種,關于Promise介紹的比較簡略,決定再詳細總結一下,既是對上一篇文章的補充,也能以更深刻的方式分享自己關于異步編程的理解。
前言如果你有志于成為一個優秀的前端工程師,或是想要深入學習JavaScript,異步編程是必不可少的一個知識點,這也是區分初級,中級或高級前端的依據之一。如果你對異步編程沒有太清晰的概念,那么我建議你花點時間學習JavaScript異步編程,如果你對異步編程有自己的獨特理解,也歡迎閱讀本文,一起交流。
同步與異步介紹異步之前,回顧一下,所謂同步編程,就是計算機一行一行按順序依次執行代碼,當前代碼任務耗時執行會阻塞后續代碼的執行。
同步編程,即是一種典型的請求-響應模型,當請求調用一個函數或方法后,需等待其響應返回,然后執行后續代碼。
一般情況下,同步編程,代碼按序依次執行,能很好的保證程序的執行,但是在某些場景下,比如讀取文件內容,或請求服務器接口數據,需要根據返回的數據內容執行后續操作,讀取文件和請求接口直到數據返回這一過程是需要時間的,網絡越差,耗費時間越長,如果按照同步編程方式實現,在等待數據返回這段時間,JavaScript是不能處理其他任務的,此時頁面的交互,滾動等任何操作也都會被阻塞,這顯然是及其不友好,不可接受的,而這正是需要異步編程大顯身手的場景,如下圖,耗時任務A會阻塞任務B的執行,等到任務A執行完才能繼續執行B:
當使用異步編程時,在等待當前任務的響應返回之前,可以繼續執行后續代碼,即當前執行任務不會阻塞后續執行。
異步編程,不同于同步編程的請求-響應模式,其是一種 事件驅動編程 ,請求調用函數或方法后,無需立即等待響應,可以繼續執行其他任務,而之前任務響應返回后可以通過狀態、通知和回調來通知調用者。
多線程
前面說明了異步編程能很好的解決同步編程阻塞的問題,那么實現異步的方式有哪些呢?通常實現異步方式是多線程,如C#, 即同時開啟多個線程,不同操作能并行執行,如下圖,耗時任務A執行的同時,在線程二中任務B也可以執行:
JavaScript單線程
JavaScript語言執行環境是單線程的,單線程在程序執行時,所走的程序路徑按照連續順序排下來,前面的必須處理好,后面的才會執行,而使用異步實現時,多個任務可以并發執行。那么JavaScript的異步編程如何實現呢,下一節將詳細闡述其異步機制。
并行與并發
前文提到多線程的任務可以并行執行,而JavaScript單線程異步編程可以實現多任務并發執行,這里有必要說明一下并行與并發的區別。
并行,指同一時刻內多任務同時進行; 并發,指在同一時間段內,多任務同時進行著,但是某一時刻,只有某一任務執行;通常所說的并發連接數,是指瀏覽器向服務器發起請求,建立TCP連接,每秒鐘服務器建立的總連接數,而假如,服務器處10ms能處理一個連接,那么其并發連接數就是100。
JavaScript異步機制本節介紹JavaScript異步機制,首先來看一個例子:
for (var i = 0; i < 5; i ++) {setTimeout(function(){ console.log(i);}, 0); } console.log(i); //5 ; 5 ; 5 ; 5; 5
應該明白最后輸出的全是5:
i在此處是for循環所在上下文環境的變量,有且只有一個i; 循環結束時i==5; JavaScript單線程事件處理器在線程空閑前不會執行下一事件。如上面第三點所述,如果要真正理解以上例子中的setTimeout(),及JavaScript異步機制,需要理解JavaScript的事件循環和并發模型。
并發模型(Concurrency model)目前,我們已經知道,JavaScript執行異步任務時,不需要等待響應返回,可以繼續執行其他任務,而在響應返回時,會得到通知,執行回調或事件處理程序。那么這一切具體是如何完成的,又以什么規則或順序運作呢?接下來我們需要解答這個問題。
注:回調和事件處理程序本質上并無區別,只是在不同情況下,不同的叫法。
前文已經提到,JavaScript異步編程使得多個任務可以并發執行,而實現這一功能的基礎是JavScript擁有一個基于事件循環的并發模型。
堆棧與隊列
介紹JavaScript并發模型之前,先簡單介紹堆棧和隊列的區別:
堆(heap):內存中某一未被阻止的區域,通常存儲對象(引用類型); 棧(stack):后進先出的順序存儲數據結構,通常存儲函數參數和基本類型值變量(按值訪問); 隊列(queue):先進先出順序存儲數據結構。 事件循環(Event Loop)JavaScript引擎負責解析,執行JavaScript代碼,但它并不能單獨運行,通常都得有一個宿主環境,一般如瀏覽器或Node服務器,前文說到的單線程是指在這些宿主環境創建單一線程,提供一種機制,調用JavaScript引擎完成多個JavaScript代碼塊的調度,執行(是的,JavaScript代碼都是按塊執行的),這種機制就稱為事件循環(Event Loop)。
注:這里的事件與DOM事件不要混淆,可以說這里的事件包括DOM事件,所有異步操作都是一個事件,諸如ajax請求就可以看作一個request請求事件。
JavaScript執行環境中存在的兩個結構需要了解:
消息隊列(message queue),也叫任務隊列(task queue):存儲待處理消息及對應的回調函數或事件處理程序; 執行棧(execution context stack),也可以叫執行上下文棧:JavaScript執行棧,顧名思義,是由執行上下文組成,當函數調用時,創建并插入一個執行上下文,通常稱為執行棧幀(frame),存儲著函數參數和局部變量,當該函數執行結束時,彈出該執行棧幀;注:關于全局代碼,由于所有的代碼都是在全局上下文執行,所以執行棧頂總是全局上下文就很容易理解,直到所有代碼執行完畢,全局上下文退出執行棧,棧清空了;也即是全局上下文是第一個入棧,最后一個出棧。
任務
分析事件循環流程前,先闡述兩個概念,有助于理解事件循環:同步任務和異步任務。
任務很好理解,JavaScript代碼執行就是在完成任務,所謂任務就是一個函數或一個代碼塊,通常以功能或目的劃分,比如完成一次加法計算,完成一次ajax請求;很自然的就分為同步任務和異步任務。同步任務是連續的,阻塞的;而異步任務則是不連續,非阻塞的,包含異步事件及其回調,當我們談及執行異步任務時,通常指執行其回調函數。
事件循環流程
關于事件循環流程分解如下:
宿主環境為JavaScript創建線程時,會創建堆(heap)和棧(stack),堆內存儲JavaScript對象,棧內存儲執行上下文; 棧內執行上下文的同步任務按序執行,執行完即退棧,而當異步任務執行時,該異步任務進入等待狀態(不入棧),同時通知線程:當觸發該事件時(或該異步操作響應返回時),需向消息隊列插入一個事件消息; 當事件觸發或響應返回時,線程向消息隊列插入該事件消息(包含事件及回調); 當棧內同步任務執行完畢后,線程從消息隊列取出一個事件消息,其對應異步任務(函數)入棧,執行回調函數,如果未綁定回調,這個消息會被丟棄,執行完任務后退棧; 當線程空閑(即執行棧清空)時繼續拉取消息隊列下一輪消息(next tick,事件循環流轉一次稱為一次tick)。使用代碼可以描述如下:
var eventLoop = []; var event; var i = eventLoop.length - 1; // 后進先出 while(eventLoop[i]) {event = eventLoop[i--]; if (event) { // 事件回調存在 event();}// 否則事件消息被丟棄 }
這里注意的一點是等待下一個事件消息的過程是同步的。
并發模型與事件循環
var ele = document.querySelector(’body’); function clickCb(event) {console.log(’clicked’); } function bindEvent(callback) {ele.addEventListener(’click’, callback); } bindEvent(clickCb);
針對如上代碼我們可以構建如下并發模型:
如上圖,當執行棧同步代碼塊依次執行完直到遇見異步任務時,異步任務進入等待狀態,通知線程,異步事件觸發時,往消息隊列插入一條事件消息;而當執行棧后續同步代碼執行完后,讀取消息隊列,得到一條消息,然后將該消息對應的異步任務入棧,執行回調函數;一次事件循環就完成了,也即處理了一個異步任務。
再談setTimeout(…0)
了解了JavaScript事件循環后我們再看前文關于 setTimeout(...0) 的例子就比較清晰了:
setTimeout(...0) 所表達的意思是:等待0秒后(這個時間由第二個參數值確定),往消息隊列插入一條定時器事件消息,并將其第一個參數作為回調函數;而當執行棧內同步任務執行完畢時,線程從消息隊列讀取消息,將該異步任務入棧,執行;線程空閑時再次從消息隊列讀取消息。
再看一個實例:
var start = +new Date(); var arr = []; setTimeout(function(){console.log(’time: ’ + (new Date().getTime() - start)); },10); for(var i=0;i<=1000000;i++){arr.push(i); }
執行多次輸出如下:
在 setTimeout 異步回調函數里我們輸出了異步任務注冊到執行的時間,發現并不等于我們指定的時間,而且兩次時間間隔也都不同,考慮以下兩點:
在讀取消息隊列的消息時,得等同步任務完成,這個是需要耗費時間的; 消息隊列先進先出原則,讀取此異步事件消息之前,可能還存在其他消息,執行也需要耗時;所以異步執行時間不精確是必然的,所以我們有必要明白無論是同步任務還是異步任務,都不應該耗時太長,當一個消息耗時太長時,應該盡可能的將其分割成多個消息。
Web Workers
每個Web Worker或一個跨域的iframe都有各自的堆棧和消息隊列,這些不同的文檔只能通過postMessage方法進行通信,當一方監聽了message事件后,另一方才能通過該方法向其發送消息,這個message事件也是異步的,當一方接收到另一方通過postMessage方法發送來的消息后,會向自己的消息隊列插入一條消息,而后續的并發流程依然如上文所述。
JavaScript異步實現關于JavaScript的異步實現,以前有:回調函數,發布訂閱模式,Promise三類,而在ES6中提出了生成器(Generator)方式實現,關于回調函數和發布訂閱模式實現可參見另一篇文章,后續將推出一篇詳細介紹Promise和Generator。
參考:
Concurrency model and Event Loop
來自:http://blog.codingplayboy.com/2017/04/25/js_async/
相關文章:
1. 使用.net core 自帶DI框架實現延遲加載功能2. php網絡安全中命令執行漏洞的產生及本質探究3. Angular獲取ngIf渲染的Dom元素示例4. php面向對象程序設計介紹5. ASP調用WebService轉化成JSON數據,附json.min.asp6. 無線標記語言(WML)基礎之WMLScript 基礎第1/2頁7. 三個不常見的 HTML5 實用新特性簡介8. php測試程序運行速度和頁面執行速度的代碼9. Warning: require(): open_basedir restriction in effect,目錄配置open_basedir報錯問題分析10. ASP.NET Core 5.0中的Host.CreateDefaultBuilder執行過程解析
