天天看點

[Effective JavaScript 筆記]第62條:在異步序列中使用嵌套或命名的回調函數

異步程式的操作順序

61條講述了異步API如何執行潛在的代價高昂的I/O操作,而不阻塞應用程式繼續處理其他輸入。了解異步程式的操作順序剛開始有點混亂。例如,下面的代碼會在列印"finished"之前列印“starting”,即使這兩個動作的程式源檔案中以相反的順序呈現。

downloadAsync('file.txt',function(file){
    console.log('finished');
});
console.log('starting');
           

downloadAsync調用會立即傳回,不會等待檔案完成下載下傳。同時,js的運作到完成機制確定下一行代碼會在處理其他事件處理程式之前被執行。也就是說"starting"一定會在"finished"之前被列印。

了解操作序列的最簡單的方式是異步API是發起操作而不是執行操作。上面的代碼發起了一個檔案的下載下傳然後立即列印了“starting”。當下載下傳完成後,在事件循環的某個單獨的輪次中,被注冊的事件處理程式才會列印出“finished”。

如何串聯異步操作

如果你需要在發起一個操作後做一些事情,如果隻能在一行中放置好幾個聲明,那麼如何串聯已完成的異步操作呢?例如,如果我們需要在異步資料庫中查找一個URL,然後下載下傳這個URL的内容?不可能發起兩個連續的請求。

db.lookupAsync('url',function(url){
    
});
downloadAsync(url,function(text){//error:url is bound
    console.log('contents of ' + url +':'+text);
});
           

以上代碼不可能工作,因為從資料庫查詢到的URL結果需要作為downloadAsync方法的參數。但是它并不在作用域内。我們所做的這一步隻是發起資料庫查找,查找的結果還不可用。

回調函數處理

最簡單的處理方法使用嵌套。借助于閉包的魔力,将第二個動作嵌套在第一個動作的回調函數中。

db.lookupAsync('url',function(url){
    downloadAsync(url,function(text){
       console.log('contents of ' + url +':'+text);
    });
})
           

這裡有兩個回調函數,但第二個被包含在第一個中,建立閉包能夠通路外部回調函數的變量。

嵌套的異步操作很容易,但當擴充到更長的序列時會很快變得麻煩。

db.lookupAsync('url',function(url){
    downloadAsync(url,function(file){
        downloadAsync('a.txt',function(a){
            downloadAsync('b.txt',function(b){
                downloadAsync('c.txt',function(c){
                    //....
                });
            });
        });
    });
});
           

回調命名的函數

減少過多的嵌套的方法之一是将嵌套的回調函數作為命名的函數,并将它們需要附加資料作為額外的參數傳遞。以上代碼可以改寫為:

db.lookupAsync('url',downloadURL);
function downloadURL(url){
    downloadAsync(url,function(text){
        showContents(url,text);
    });
}
function showContents(url,text){
    console.log('contents of ' + url +':'+text);
}
           

使用bind方法消除嵌套

為了合并外部的url變量和内部的text變量作為showContents方法的參數,在downloadURL方法中仍然使用了嵌套的回調函數。這裡可以使用bind方法消除最深層的嵌套回調函數。

db.lookupAsync('url',downloadURL);
function downloadURL(url){
    downloadAsync(url,showContents.bind(null,url));
}
function showContents(url,text){
    console.log('contents of ' + url +':'+text);
}
           

這種做法可以使代碼看起來很有順序性,但需要為操作序列的每個中間步驟命名,并且一步步地使用綁定。這可能導緻尴尬的情況,如多層嵌套時。

db.lookupAsync('url',downloadURLAndFiles);
function downloadURLAndFiles(url){
    downloadAsync(url,downloadABC.bind(null,url));
}

function downloadABC(url,file){
    downloadAsync('a.txt',downloadBC.bind(null,url,file));
}

function downloadBC(url,file,a){
    downloadAsync('b.txt',downloadC.bind(null,url,file,a));
}
function downloadC(url,file,a,b){
    downloadAsync('c.txt',finish.bind(null,url,file,a,b));
}
function finish(url,file,a,b,c){
    //....
}
           

結合兩種方法

結合這兩種方法,會使代碼更易了解。

db.lookupAsync('url',function(url){
   downloadURLAndFiles(url); 
});

function downloadURLAndFiles(url){
    downloadAsync(url,downloadFiles.bind(null,url));
}
function downloadFiles(url,file){
    downloadAsync('a.txt',function(a){
        downloadAsync('b.txt',function(b){
            downloadAsync('c.txt',function(c){
                //...
            });
        });
    });
}
           

最後一步可以使用一個額外的抽象來簡化,可以下載下傳多個檔案并将它們存儲在數組中。

function downloadFiles(url,file){
    downloadAllAsync(['a.txt','b.txt','c.txt'],function(all){
        var a=all[0],b=all[1],c=all[2];
    });
}
           

使用downloadAllAsync函數允許我們同時下載下傳多個檔案。排序意味着每個操作隻有等前一個操作完成後才能啟動。一些操作本質上是連續的,比如下載下傳我們從資料庫查詢到的URL。但如果我們有一個檔案清單要下載下傳,沒理由等每個檔案完成下載下傳後才請求接下來的一個。

除了嵌套和命名回調,還可以建立更高層的抽象使異步控制流更簡單、更簡潔。

提示

  • 使用嵌套或命名的回調函數按順序地執行多個異步操作
  • 嘗試在過多的嵌套的回調函數和尴尬的命名的非嵌套回調函數之間取得平衡
  • 避免将可被并行執行的操作順序化

版權聲明

翻譯的文章,版權歸原作者所有,隻用于交流與學習的目的。

原創文章,版權歸作者所有,非商業轉載請注明出處,并保留原文的完整連結。

繼續閱讀