設想有downloadAsync函數的一種變種,它持有一個緩存(實作為一個Dict)來避免多次下載下傳同一個檔案。在檔案已經被緩存的情況下,立即調用回調函數是最優選擇。
var cache=new Dict();
function downloadCachingAsync(url,onsuccess,onerror){
if(cache.has(url)){
onsuccess(cache.get(url));
return;
}
return downloadAsync(url,function(file){
cache.set(url,file);
onsuccess(file);
},onerror);
}
通常情況下,它會立即提供資料,但這種方式是違反了異步API用戶端的期望。首先,它改變了操作的預期順序。第62條顯示了下面的例子,對于循規蹈矩的異步API應該總是以一種可預測的順序來記錄日志消息。
downloadAsync('file.txt',function(file){
console.log('finished');
});
console.log('starting');
使用上面的downloadCachingAsync實作,這樣的用戶端代碼可能最終會以任意的順序記錄事件,這取決于檔案是否已被緩存起來。
downloadCachingAsync('file.txt',function(file){
console.log('finished');
});
console.log('starting');
日志消息的順序是一回事。更一般的是,異步API的目的是維持事件循環中每輪的嚴格分離。正如第61條解釋的,這簡化了并發,通過減輕每輪事件循環的代碼量而不必擔心其他代碼并發地修改共享的資料結構。同步地調用異步的回調函數違反了這一分離,導緻在目前輪完成之前,代碼用于執行一輪隔離的事件循環。
例如,應用程式可能會持有一個剩餘的檔案隊列給使用者下載下傳和顯示消息。
downloadCachingAsync(remaining[0],function(file){
remaining.shift();
});
status.display('Downloading '+remaining[0]+'...');
如果同步地調用該回調函數,那麼将顯示錯誤的檔案名的消息(或者更糟糕的是,如果隊列為空會顯示“undefined”)。
同步的調用異步的回調函數甚至可能會導緻一些微妙的問題。第64條解釋了異步的回調函數本質上是以空的調用棧來調用,是以将異步的循環實作為遞歸函數是安全的,完全沒有累積超越調用棧空間的危險。同步的調用不能保障這一點,因而使得一個表面上的異步循環很可能會耗盡調用棧空間。另一種問題是異常。對于上面的downloadCachingAsync實作,如果回調函數抛出一個異常,它将會在每輪的事件循環中,也就是開始下載下傳時而不是期望的一個分離的回合中抛出該異常。
為了確定總是異步地調用回調函數,我們可以使用已存在的異步API。就像我們在第65條和第66條中所做的一樣,我們使用通用的庫函數setTimeout在每隔一個最小的逾時時間後給事件隊列增加一個回調函數。可能有比setTimeout函數更完美的替代方案來排程即時事件,這取決于特定平台。
var cache=new Dict();
function downloadCachingAsync(url,onsuccess,onerror){
if(cache.has(url)){
var cache=cache.get(url);
setTimeout(onsuccess.bind(null,cached),0);
return;
}
return downloadAsync(url,function(file){
cache.set(url,file);
onsuccess(file);
},onerror);
}
這裡使用bind函數将結果儲存為onsuccess回調函數的第一個參數。
提示
- 即使可以立即得到資料,也絕不要同步地調用異步回調函數
- 同步地調用異步的回調函數擾亂了預期的操作序列,并可能導緻意想不到的交錯代碼
- 同步地調用異步的回調函數可能導緻棧溢出或錯誤地處理異常
- 使用異步的API,比如setTimeout函數來排程異步回調函數,使其運作于另一回合
版權聲明
翻譯的文章,版權歸原作者所有,隻用于交流與學習的目的。
原創文章,版權歸作者所有,非商業轉載請注明出處,并保留原文的完整連結。