基于瀏覽器的事件輪詢機制(以及Node.js中的事件輪詢機制),Javascript常常會運行在異步環(huán)境中。由于JavaScript本身語言的特性(不需要程序員操控線程/進程),在js中解決異步化編程的方法就顯得相當重要。可以說一個完整的項目中,js開發(fā)人員是不可能不面對異步操作的。本文將詳細介紹幾種經(jīng)典JavaScript異步編程串行化方法,同時也將簡單介紹一下ES6提供的PRomise順序執(zhí)行方法。
假設我們有一個Ajax()
方法,他接收一個url參數(shù),向該地址發(fā)起一個異步請求,在請求結(jié)束時執(zhí)行第二個參數(shù)—一個回調(diào)函數(shù):
可以說這種方式幾乎是每個前端開發(fā)人員都用過的回調(diào)函數(shù)方式,有了這樣的回調(diào)機制,開發(fā)人員就不用編寫類似下面這樣的代碼來推測服務器請求什么時候返回:
var result=ajax(url);setTimeout(function(result){ console.log(result);},400);大家應該能明白我此處想表達的意思。我們設置了一個延遲400毫秒的定時器,來假設我們發(fā)出的ajax請求會在400毫秒之內(nèi)完成。否則,我們將會操作一個undefined
的result
。 但是有一個問題隨著項目的擴大漸漸浮現(xiàn)出來:如果場景需要我們多層嵌套回調(diào)函數(shù),代碼將變得難以閱讀和維護:
為了解決內(nèi)聯(lián)回調(diào)函數(shù)暴露出來的代碼混亂問題,我們引入外部函數(shù)調(diào)用來解決類似問題:
function handle2(result){ console.log(result);}function handle1(result){ ajax(result.url,function(result){ handle2(result); });}ajax(url,function(result){ handle1(result);});通過這種拆分內(nèi)聯(lián)函數(shù),來調(diào)用外部函數(shù)的優(yōu)化方法,能極大的保持代碼的簡潔性。
觀察流行的JavaScript流程控制工具,例如Nimble、Step、Seq,我們會學習到一種簡潔的設計模式:通過回調(diào)管理器來控制異步JavaScript執(zhí)行流程,下面是一個典型的回調(diào)管理器的關(guān)鍵代碼示例:
var Flow={};//設置next方法,在上一個方法完成時調(diào)用下一個方法Flow.next=function(){ if(this.stack[0]){ //彈出方法棧中的第一個方法,并執(zhí)行他 this.stack.shift()(); }};//設置series方法,接收一個函數(shù)數(shù)組,并按序執(zhí)行Flow.series=function(arr){ this.stack=arr; this.next();};//通過Flow.series我們能夠控制傳入的函數(shù)的執(zhí)行順序Flow.series([ function(){ //do something console.log(1); Flow.next(); }, function(next){ //do something console.log(2); Flow.next(); }]);我們初始化了一個Flow
控制器,為他設計了series
和next
兩個函數(shù)屬性。在我們編寫的業(yè)務方法內(nèi)部,在方法結(jié)尾處通過調(diào)用Flow.next()
的方式來順序觸發(fā)下一個方法;通過執(zhí)行series
方法來順序執(zhí)行異步函數(shù)。這種通過核心控制器來管理異步函數(shù)調(diào)用的方式簡化了我們的編程過程,讓開發(fā)人員能夠投入更多精力在業(yè)務邏輯上。
也許上面介紹的異步方法仍然不能滿足實際開發(fā)中的業(yè)務場景:假設我們有a()
,b()
,c()
三個方法,a和b沒有依賴關(guān)系,可以異步進行。但是c必須在a和b都完成之后才能觸發(fā)。為滿足這樣的邏輯實現(xiàn),我們加入一個全局計數(shù)器來控制代碼的執(zhí)行流程:
我們設置了一個全局變量flag來監(jiān)控方法a和方法b的完成情況。方法b通過設置一個200毫秒的定時器來模擬網(wǎng)絡環(huán)境,最終會在b方法執(zhí)行完成之后成功調(diào)用c方法。這樣我們就實現(xiàn)了對方法a()
,b()
,c()
的依賴調(diào)用。
當上述方案在復雜場景下應用時,會出現(xiàn)如下問題:產(chǎn)品經(jīng)過多個版本迭代,c方法依賴更多的方法,因此計數(shù)器flag需要不斷的變化;產(chǎn)品迭代過程中更換了開發(fā)人員。當出現(xiàn)上述兩種情況時,代碼的邏輯將會變得混亂不堪,flag標記符是否能保持簡明正確很大程度上受到了產(chǎn)品迭代的影響。因此我們提出面向數(shù)據(jù)的優(yōu)化改進。 在真實的開發(fā)場景中,存在方法依賴的原因基本都是因為存在數(shù)據(jù)依賴,對于上面那個簡單的示例:c方法依賴于a方法和b方法操作的結(jié)果,而不是依賴于flag是否為0。因此我們可以通過檢查依賴方法是否已經(jīng)完成了數(shù)據(jù)處理來代替檢查標記符是否已經(jīng)被置為0,在這個例子中也就是在c方法中檢查aValue和bValue是否已經(jīng)完成了賦值:
function c(){ if(aValue!==undefined && bValue!==undefined){ console.log("after a and b:"+(aValue+bValue)); }}針對更加通用的場景,我們將上述代碼修改為下:
var checkDependency={};var aValue,bValue;function a(){ aValue=1; checkDependency.a=true; c();}function b(){ setTimeout(function(){ bValue=2; checkDependency.b=true; c(); },200);}function c(){ if(checkDependency.a && checkDependency.b){ console.log("after a and b:"+(aValue+bValue)); }}a();b();通過面向數(shù)據(jù)的檢查方式,未來擴展時,我們僅需要在新增的方法中增加對checkDependency
對象的修改,并且在c方法中檢查相應屬性的存在就能實現(xiàn)異步依賴方法的順序執(zhí)行。
為了解決JavaScript中異步方法的復雜性,官方引入了一種統(tǒng)一的控制方式:
var bool=false;/* * 新建一個Promise實例,向構(gòu)造函數(shù)傳入一個異步執(zhí)行函數(shù) * 異步函數(shù)會接受兩個參數(shù),由Promise傳入,對應then方法中傳入的方法 */var promise=new Promise(function(resolve,reject){ setTimeout(function(){ if(bool){ //根據(jù)執(zhí)行情況相應調(diào)用resolve和reject resolve(bool); }else{ reject(bool); } },200);});//通過then向Promise實例傳入解決方法promise.then(function resolve(result){ console.log("success");},function reject(result){ console.log("failure");});上例代碼展示了一個基礎的Promise應用,也許實際場景中更加多見的是下面這種鏈式調(diào)用:
new Promise(function(res,rej){ if(/*異步調(diào)用成功*/){ res(data); }else{ rej(error); }}).then(function resolve(result){ console.log("success");},function reject(result){ console.log("failure");});如果對Promise感興趣的話,可以在網(wǎng)上尋找資料繼續(xù)深入學習! 關(guān)于Promise的兼容性,通常web前端JavaScript代碼中不會直接使用Promise(通過caniuse.com網(wǎng)站查詢發(fā)現(xiàn)Android4.4不支持Promise)。如果特別想使用的,往往會在項目中附帶一些補足兼容性的promise類庫;而后端Node.js可以放心使用Promise類來管理異步邏輯。
新聞熱點
疑難解答