第63條建議使用工具函數downloadAllAsync接收一個URL數組並下載所有文件,結果返回一個存儲了文件內容的數組,每個URL對應一個字元串。downloadAllAsync並不只有清理嵌套回調函數的好處,其主要好處是並行下載文件。我們可以在同一個事件迴圈中一次啟動所有文件的下載,而不用等待... ...
第63條建議使用工具函數downloadAllAsync接收一個URL數組並下載所有文件,結果返回一個存儲了文件內容的數組,每個URL對應一個字元串。downloadAllAsync並不只有清理嵌套回調函數的好處,其主要好處是並行下載文件。我們可以在同一個事件迴圈中一次啟動所有文件的下載,而不用等待每個文件完成下載。
並行邏輯是微妙的,很容易出錯。下麵有實現有一個隱藏的缺陷。
function downloadAllAsync(urls,onsuccess,onerror){
var result=[],length=urls.length;
if(length === 0){
setTimeout(onsuccess.bind(null,result),0);
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut.push(text);
if(result.length===urls.length){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}
這個函數有嚴重的錯誤,但首先讓我們看看它是如何工作的。先確保如果數組是空的,則會使用空結果數組調用回調函數。如果不這樣做,這兩個回調函數將不會被調用,因為forEach迴圈是空的。接下來,遍歷整個URL數組,為每個URL請求一個非同步下載。每次下載成功,就將文件內容加入到result數組中。如果所有URL都被成功下載,使用result數組調用onsuccess回調函數。如果有任何失敗的下載,使用錯誤值調用onerror回調函數。如果有多個下載失敗,設置result數組為null,從而保證onerror只被調用一次,即在第一次錯誤發生時。
錯誤示例
var filenames=[
'huge.txt',
'tiny.txt',
'medium.txt'
];
downloadAllAsync(filenames,function(files){
console.log('Huge file:'+files[0].length);//tiny
console.log('Tiny file:'+files[1].length);//medium
console.log('Medium file:'+files[2].length);//huge
},function(error){
console.log('Error: '+error);
});
由於這些文件是並行下載的,事件可以以任意的順序發生(因些被添加到應用程式事件序列)。例如,如果tiny.txt先下載完成,接下來是medium.txt文件,最後是buge.txt文件,則註冊到downloadAllAsync的回調函數並不會按照它們被創建的順序進行調用。但downloadAllAsync的實現是一旦下載完成就立即將中間結果保存在result數組的末尾。所以downloadAllAsync函數提供的保存下載文件內容的數組的順序是未知的。這個API幾乎不可用,因為無法確認哪個結果對應哪個文件。
程式的執行順序不能保證與事件發生的順序一致。
當一個應用程式依賴於特定的事件順序才能正常工作時,這個程式會遭受數據競爭。數據競爭是指多個併發操作可以修改共用的數據結構,這取決於它們發生的順序。數據競爭是真正棘手的錯誤。它們可能不會出現於特定的測試中,因為運行相同的程式兩次,每次可能會得不到不同的結果。例如downloadAllAsync的使用者可能會對文件重新排序,基於的順序是哪個文件可能會最先完成下載。
downloadAllAsync(filenames,function(files){
console.log('Huge file:'+files[2].length);
console.log('Tiny file:'+files[0].length);
console.log('Medium file:'+files[1].length);
},function(error){
console.log('Error: '+error);
});
在這種情況下大多數時候結果是相同的順序,但偶爾由於改變了伺服器負載均衡或網路緩存,文件可能不是期望的順序。我們可以順序下載文件,但也失去了併發的性能優勢。
下麵實現downloadAllAsync不依賴不可預期的事件執行順序而總能提供預期結果。我們不將每個結果放置到數組末尾,而是存儲在其原始的索引位置。
function downloadAllAsync(urls,onsuccess,onerror){
var result=[],length=urls.length;
if(length === 0){
setTimeout(onsuccess.bind(null,result),0);
return;
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut[i]=text;
if(result.length===urls.length){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}
該實現利用了forEach回調函數的第二個參數。第二個參數為當前迭代提供了數組索引。這也不正確。第51條描述數組更新的契約,即設置一個索引屬性,總是確保數組的length屬性值大於索引。假設有如下的一個請求。
downloadAllAsync(['huge.txt','medium.txt','tiny.txt']);
如果tiny.txt文件最先被下載,結果數組將獲取索引為2的屬性,這將導致result.length被更新為3。用戶的success回調函數將被過早地調用,其參數為一個不完整的結果數組。
正確的實現應該是使用一個計數器來追蹤正在進行的操作數量。
function downloadAllAsync(urls,onsuccess,onerror){
var pending=urls.length;
var result=[];
if(pending === 0){
setTimeout(onsuccess.bind(null,result),0);
return;
}
urls.forEach(function(url){
downloadAsync(url,function(text){
if(result){
reslut[i]=text;
pending--;
if(pending===0){
onsuccess(result);
}
}
},function(error){
if(result){
result=null;
onerror(error);
}
});
});
}
現在不論事件以什麼樣的順序發生,pending計數器都能準確地指出何時所有的事件會被完成,並以適當的順序返回完整的結果。
提示
-
js應用程式中的事件發生是不確定的,即順序是不可預測的
-
使用計數器避免並行操作中的數據競爭