假設需要有這樣一個函數,接收一個URL的數組并嘗試依次下載下傳每個檔案直到有一個檔案被成功下載下傳。如果API是同步的,使用循環很簡單實作。
function downloadOneSync(urls){
for(var i=0,n=urls.length;i< n;i++){
try{
return downloadSync(urls[i]);
}catch(e){}
}
throw new Error('all downloads failed.');
}
在異步情況下,上面的這種方式就無法正确工作。因為不能在回調函數中暫停循環并恢複。如果嘗試使用循環,它将啟動所有的下載下傳,這不是等待完成一個再進行下一個。
function downloadOneAsync(urls,onsucess,onerror){
for(var i=0,n=urls.length;i < n;i++){
downloadAsync(urls[i],onsucess,function(error){
//?
});
//loop continues
}
throw new Error('all downloads failed');
}
這裡我們要實作一個類似循環的東西,我們需要顯式地說繼續執行,它才會繼續執行。解決方案是将循環實作為一個函數,可以決定何時開始每次疊代。
function downloadOneAsync(urls,onsucess,onfailure){
var n=urls.length;
function tryNextURL(i){
if(i>=n){
onfailure('all downloads failed');
return;
}
downloadAsync(urls[i],onsuccess,function(){
tryNextURL(i+1);
});
}
tryNextURL(0);
}
局部函數tryNextURL是一個遞歸函數。它的實作調用了其自身。典型的javascript環境中一個遞歸函數同步調用自身過多次會導緻失敗。例如,下例中的遞歸函數試圖調用自身10萬次,在大多數的js環境中會産生一個運作時錯誤。
function countdown(n){
if(n===0){
return 'done';
} else {
return countdown(n-1);
}
}
當n太大時countdown函數會執行失敗,那麼如何確定downloadOneAsync函數是安全的呢?檢視一下countdown函數提供的錯誤資訊。
VM58:1 Uncaught RangeError: Maximum call stack size exceeded(…)
js環境通常在記憶體中儲存一塊固定的區域,稱為調用棧,用于記錄函數調用傳回前下一步該做什麼。執行下面的小程式。
function negative(x){
return abs(x)*-1;
}
function abs(x){
return Math.abs(x);
}
console.log(negative(42));
當程式使用參數42調用Math.abs方法時,有幾個其他的函數調用也在進行,每個都在等待另一個的調用傳回。在每個函數調用時,項目符号(.)描述了在程式中已經發生的函數調用地方及這次調用完成後将傳回哪裡。就像傳統的棧資料結構,這個資訊遵循“先進後出”協定。最新的函數調用将資訊推入棧(被表示為棧的最底層的幀),該資訊也将首先從棧中彈出。當Math.abs執行完畢,将會傳回給abs函數,其将傳回給negative函數,然後将傳回到最外面的腳本。
當一個程式執行中有太多的函數調用,它會耗盡棧空間,最終抛出異常。這種情況被稱為棧溢出。在此例中,調用countdown(10萬次)需要countdown調用自身10萬次,每次推入一個棧桢。存儲這麼多棧幀需要的空間量會耗盡大多數js環境配置設定空間,導緻運作時錯誤。
現在再看看downloadOneAsync函數。不像countdown直到遞歸調用傳回後才會傳回,downloadOneAsync隻在異步回調函數中調用自身。記住異步API在其回調函數被調用前會立即傳回。是以downloadOneAsync傳回,導緻其棧幀在任何遞歸調用将新的棧幀推入棧前,會從調用棧中彈出。(事實上,回調函數總在事件循環的單獨輪次中被調用,事件循環的每個輪次中調用其他事件處理程式的調用棧最初是空的。)是以無論downloadOneAsync需要多少次疊代,都不會耗盡棧空間。
提示
- 循環不能是異步的
- 使用遞歸函數在事件循環的單獨輪次中執行疊代
- 在事件循環的單獨輪次中執行遞歸,并不會導緻調用棧溢出
版權聲明
翻譯的文章,版權歸原作者所有,隻用于交流與學習的目的。
原創文章,版權歸作者所有,非商業轉載請注明出處,并保留原文的完整連結。