js程式是構建在事件之上的。輸入可能來自不同的外部源。在一些語言中,我們習慣地編寫代碼來等待某個特定的輸入。var text=downloadSync('http://example.com/file.txt'); console.log(text); 像這樣的形式downloadSync稱為同步函... ...
js程式是構建在事件之上的。輸入可能來自不同的外部源。在一些語言中,我們習慣地編寫代碼來等待某個特定的輸入。
var text=downloadSync('http://example.com/file.txt');
console.log(text);
像這樣的形式downloadSync稱為同步函數(或阻塞函數)。程式會停止做任何工作,而等待它的輸入。在這個例子中,也就是等待從網路上下載文件的結果。由於在等待下載完成的期間,電腦可以做其他有用的工作,因此這樣的語言通常為程式員提供一種方法來創建多個線程,即並行執行子計算。它允許程式的一部分停下來等待(阻塞)一個低速的輸入,而程式的另一部分可以繼續進行獨立的工作。
在js中,大多的I/O操作都提供了非同步的或非阻塞的API。程式員提供一個回調函數,一旦輸入完成就可以被系統調用,而不是程式阻塞在等待結果的線程上。
var text=downloadSync('http://example.com/file.txt',function(text){
console.log(text);
});
該API初始化下載進行,然後在內部註冊表中存儲了回調函數後立即返回,而不是被網路請求阻塞。當下載完成之後,系統會將下載完的文件的文本作為參數調用該已註冊的回調函數。
隨著事件的發生,事件被添加到應用程式的事件隊列的末尾。js系統使用一個內部迴圈機制來執行應用程式。該迴圈機制每次都拉取隊列底部的事件,以接收到這些事件的順序來調用這些已經註冊的js事件處理程式,並將事件的數據作為該事件處理程式的參數。
運行到完成機制擔保的好處是當代碼運行時,你完全掌握應用程式的狀態。根本不必擔心一些變數和對象屬性的改變由於併發執行代碼而超出你的控制。併發編程在js中往往比使用線程和鎖的c++,java或c#更容易。
然而,運行到完成機制的不足是,實際上所有你編寫的代碼支撐著餘下應用程式的繼續執行。像瀏覽器這樣的互動式應用程式中,一個阻塞的事件處理程式會阻塞任何將被處理的其他用戶輸入,甚至可能阻塞一個頁面的渲染,從而導致頁面失去響應的用戶體驗。在伺服器環境中,一個阻塞的事件處理程式可能會阻塞將被處理的其他網路請求,從而導致伺服器失去響應。
js併發的一個最重要的規則是絕不要在應用程式事件隊列中使用阻塞I/O的API。在瀏覽器中,甚至幾乎沒有任何阻塞API是可用的,儘管有一些平臺實現了。提供類似於downloadAsync功能的網路I/O的XMLHttpRequest庫有一個同步的版本實現,被認為是不好的。對於web應用程式的交互性,同步的I/O會導致災難性的後果,它在I/O操作完成之前一直會阻塞用戶與頁面的交互。
相比之下,非同步的API用在基於事件的環境中是安全的,它們迫使應用程式邏輯在一個獨立的事件迴圈“輪詢”中繼續處理。如上面的例子,假設需要幾秒鐘來下載網路資源,在這段時間里,數量龐大的其他事件很可能發生。在同步的實現中,這些事件就會堆積在事件隊列中,而事件迴圈將停留等待該JS代碼執行完成,這將阻塞任何其他事件的處理。在非同步的版本中,JS代碼註冊一個事件處理程式並立即返回,這將在下載完成之前,允許其他處理程式處理這期間的事件。
在主應用程式事件隊列不受影響的環境中,阻塞操作很少出問題。例如,web平臺提供了Worker的API,該API使得產生大量的並行計算成為可能。不同於傳統的線程執行,Workers在一個完全隔離的狀態下執行,沒有獲取全局作用域或應用程式主線程web頁面內容的能力。因此,它們不會妨礙主事件隊列中運行的代碼的執行。在一個Worker中,使用XMLHttpRequest同步的變種很少出問題。下載操作的確會阻塞Worker繼續執行,但這並不會阻止頁面的渲染或事件隊列中的事件響應。在伺服器端環境中,阻塞的API在啟動一開始是沒有問題的,也就是在伺服器開始響應輸入的請求之前。然後在處理請求期間,瀏覽器事件隊列中存在阻塞的API就是有問題的啦。
提示
-
非同步API使用回調函數來延緩處理代價高昂的操作以避免阻塞主應用程式
-
js併發地接收事件,但會使用一個事件隊列按序地處理事件處理程式
-
在應用程式事件隊列中絕不要使用阻塞的I/O