Javascript 異步實現機制

Javascript 單線程指的是在一個瀏覽器進程中只存在一個 Javascript 執行線程,因此任務須要順序排列等待執行,而不能像 Java 等多線程語言同樣併發執行。可是這種單線程模型在處理耗時的異步任務是會出現較長時間的線程阻塞,致使後續的任務不能被及時處理。因此在 Javascript 中存在異步的處理方式用於處理這種狀況,不過嚴格來講所謂的異步,本質上仍是藉助於多線程的宿主實現的,併發 Javascript 語言自己特性。我想嘗試着總結一下在不一樣的宿主環境下,Javascript 的異步實現機制。html

但凡「 便是單線程又是異步 」的語言都有一個共同的特色:它們是 event-driven 的,因此 Javascript 異步的實現也與其事件機制關係密切。node

在瀏覽器端:chrome

瀏覽器端的 Javascript 實現了兩個很重要的異步 API,它們分別是 定時器AJAX請求,它們具體都是怎麼工做的呢?瀏覽器

定時器服務器

定時器好比 setTimeout 被執行時,由瀏覽器的定時器線程執行的定時計數,而並非 Javascript 執行線程負責計數,能夠想象若是是 Javascript 執行線程負責計數,那一定會形成執行線程的阻塞。定時器線程在定時時間觸發延時事件並將延時事件推入 Javascript 事件隊列。當 Javascript 主線程同步代碼執行完畢時,會去輪詢該事件隊列,取出最開始事件的處理函數推入主線程中被執行。多線程

解釋至此咱們能夠知道,爲何會說 Javascript 的定時器是不徹底準時觸發的呢?由於 Javascript 事件隊列中的任務是被順序取出執行的,若是在定時任務以前還存在其它的任務,或者主線程中的同步任務尚未被執行完畢,則定時任務會等到這以前的任務所有執行完畢以後,即主線程空閒出來了,纔會被取出執行。併發

一個關於定時器的例子異步

咱們但願在 500 ms 以後觸發定時事件,然而上面這一段代碼在 chrome 中執行的結果定時事件倒是在 1000 ms 後才被觸發。由於咱們在定時器的下面寫了一個空循環,在還不到 1000 ms 時 Javascript 主線程不會處於空閒狀態,主線程同步代碼在尚未執行完畢時,Javascript 不會去取出事件隊列中的回調執行。函數

AJAXoop

AJAX 請求和定時器相似,一樣是委託瀏覽器線程代爲執行耗時任務,這裏是藉由瀏覽器的HTTP請求線程發起對服務器的請求,在請求獲得響應以後觸發請求完成事件,將回調函數推入事件隊列等待執行。

req.send()方法是 AJAX  向服務器發生數據,它是一個異步任務,而 req.onreadystatechange()屬於事件回調,只有在主線程同步代碼執行完畢以後纔會被從事件隊列中取出執行,因此它是在 req.send()方法前面仍是後面可有可無,由於無論處於哪一個位置,它都不會被當即執行。

這讓咱們想起彷佛咱們在給 DOM 元素綁定交互事件的時候也是這樣,咱們不須要去關心在文件的哪一個區域聲明咱們的事件監聽函數。其實原理是相似的,當用戶點擊一個綁定點擊處理函數的 DOM 元素時,會有一個點擊事件排入事件隊列,該點擊事件也須要等到當前全部正在運行的代碼結束以後(可能還要等待其它此前已排隊的事件也依次結束),纔會執行。

NodeJS端:

NodeJS 的異步實現和瀏覽器端實現有所不一樣。在 NodeJS 中 Libuv 爲 Node.js 提供了跨平臺,線程池,事件池,異步 I/O 等能力,是 Node.js 如此強大的關鍵。Libuv 爲上層的 Node.js 提供了統一的 API 調用,使其不用考慮平臺差距,隱藏了底層實現。Libuv 自己就是異步和事件驅動的,因此,當咱們將 I/O 操做的請求傳達給 Libuv 以後,Libuv 開啓線程來執行此次 I/O 調用,並在執行完成後,傳回給 Javascript 進行後續處理。

總結來講,一個異步 I/O 的大體實現流程以下:

發起 I/O 調用

1 用戶經過 Javascript 代碼調用 NodeJS 核心模塊,將參數和回調傳入核心模塊

2 NodeJS 核心模塊將傳入參數和回調封裝爲一個請求對象

3 將這個請求對象推入到I/O線程池中等待執行

4 Javascript 發起的異步調用結束,Javascript 線程繼續執行後續操做

執行回調

1 異步任務完成以後,會將結果存放在請求對象的 result 屬性上,併發出操做完成通知

2 每次事件循環時會檢查 I/O 線程池中是否存在已經完成的 I/O 操做,若是有就將請求事件加入到I/O觀察者隊列當中(事件隊列),以後看成事件處理

3 處理I/O觀察者事件時,會將以前封裝在請求對象中的回調函數取出,並將 result 參數傳入執行,以完成 Javascript 回調的目的

咱們知道 NodeJS 很是適合開發 IO 密集型應用,但並不適合開發 CPU(計算) 密集型應用。爲何會這樣呢?由於 NodeJS 異步的天性,在處理併發 IO 的時候不會阻塞主線程,實際上這種異步是藉助於多線程實現的,IO 任務完成以後排隊等待主線程執行。由於 NodeJS 主線程執行同步代碼的速度很是之快,因此徹底能夠 hold 得住大規模的併發請求。但這也有存在例外,若是 NodeJS 主線程在執行同步任務的時候遇到一些計算量很是大,或者執行循環過久,等很是耗時的操做的時候,就會致使後續的代碼以及事件隊列裏已完成的 IO 任務遲遲得不到執行,嚴重拖垮 NodeJS 的性能。

完結

參考:

Node.JS探祕 初識單線程的Node.JS

JavaScript 運行機制詳解:再談Event Loop

深刻淺出 NodeJS

相關文章
相關標籤/搜索