前言
以一個煮飯的例子開始,例如有三件事,A是買菜、B是買肉、C是洗米,最終的結果是為了煮一餐飯。為了最后一餐飯,可以三件事一起做,也可以輪流做,也可能C需要最后做(等A、B做完),這三件事是相關的,抽象起來有三種場景。
順序做
先買菜,再買肉,最后洗米, 即 A->B->C。
並發做
買菜,買肉,洗米,一起做。
交集做
買菜,買肉必須先做完,才能做洗米。
場景就是這樣,接下來就是如何考慮用js實現。
function A(callback){ setTimeout(function(){ callback("買好菜了"); }, 1000); } function B(callback){ setTimeout(function(){ callback("買好肉了"); }, 2000); } function C(callback){ setTimeout(function(){ callback("洗米好了"); }, 3000); }
普通做法(無任何模式)
順序做
A(function(res){ console.log(res); B(function(res){ console.log(res); C(function(res){ console.log(res); console.log("煮飯啦~~~"); }); }); });
這就是傳說中的回調地獄,實際的場景嵌套的層級可能還更多。
並發做
var cook_count = 0; A(function(res){ callback(res); }); B(function(res){ callback(res); }); C(function(res){ callback(res); }); function callback(res){ console.log(res); cook_count++; if(cook_count == 3){ console.log("煮飯啦~~~"); } }
設置一個計數器,記錄事情發展,到計數到指定的值,即為結束。
交集做
交集做也可以使用計數器做。
var cook_count = 0; A(function(res){ callback(res); }); B(function(res){ callback(res); }); function callback(res){ console.log(res); cook_count++; if(cook_count == 2){ C(function(res){ console.log(res); console.log("煮飯啦~~~"); }); } }
發布/訂閱模式
我在上一篇文章《觀察者模式與發布/訂閱模式區別》有說明這種模式的特點,以及附上了js實現方式(這種模式有很多種實現的方式,這個只是簡單的一種)。我們可以用這種模式來解決js異步編程。
順序做
function A(){ setTimeout(function(){ pubsub.publish("A_FINISH", "買好菜了"); }, 1000); } function B(){ setTimeout(function(){ pubsub.publish("B_FINISH", "買好肉了"); }, 2000); } function C(){ setTimeout(function(){ pubsub.publish("C_FINISH", "洗米好了"); }, 3000); } A(); pubsub.subscribe("A_FINISH", B); pubsub.subscribe("B_FINISH", C); pubsub.subscribe("C_FINISH", function(){ console.log("煮飯啦~~~"); });
PS:還有一直很常用的jquery的自定義事件也是類似的寫法(on、trigger)。
並發做
訂閱/發布並沒有真正優化到異步編程,所以並發做其實還要用其他方式去檢查事情完成狀態,就如之前的計數。
var cook_count = 0; pubsub.subscribe("A_FINISH", callback); pubsub.subscribe("B_FINISH", callback); pubsub.subscribe("C_FINISH", callback); function callback(res){ console.log(res); cook_count++; if(cook_count == 3){ console.log("煮飯啦~~~"); } }
PS:交集做也是類似,就不舉例了。
Promise/Deferred模式
ES6規范包含了Promise,Promise對象可以理解為一次將要執行的操作(常常被用於異步操作),使用了 Promise 對象之后可以用一種鏈式調用的方式來組織代碼,讓代碼更加直觀。。
順序做
A().then(function(res) { console.log(res); return B(); }).then(function(res) { console.log(res); return C(); }).then(function(res) { console.log(res); console.log("煮飯啦~~~"); }); function A() { return new Promise(function (resolve, reject) { setTimeout(function(){ resolve("買好菜了"); }, 1000); }); } function B() { return new Promise(function (resolve, reject) { setTimeout(function(){ resolve("買好肉了"); }, 2000); }); } function C() { return new Promise(function (resolve, reject) { setTimeout(function(){ resolve("洗米好了"); }, 2000); }); }
then方法定義:then(fulfilledAHandler, errorHandler),對應着Promise的resolve, reject。
並發做
Promise.all方法可做並發調用,當所有Promise 對象都變為完成態或失敗態時,回調將被執行。
Promise.all([A(), B(), C()]).then(function (result) { console.log(result); //["買好菜了", "買好肉了", "洗米好了"] });
第三方框架實現
jQuery之Deferred
Deferred對象(jQuery的回調函數解決方案),對異步編程做了優化,有人總結四種功能。
一、 提供ajax操作的鏈式寫法
$.ajax("test.html") .done(function(){ alert("哈哈,成功了!"); }) .fail(function(){ alert("出錯啦!"); });
PS:高於1.5.0版本,返回的是deferred對象,可以進行鏈式操作。
二、同一操作的多個回調函數
$.ajax("test.html") .done(function(){ alert("哈哈,成功了!");} ) .done(function(){ alert("第二個回調函數!");} );
三、為多個操作指定回調函數
$.when($.ajax("test1.html"), $.ajax("test2.html")) .done(function(){ alert("哈哈,成功了!"); }) .fail(function(){ alert("出錯啦!"); });
PS:如果都成功了,就運行done()指定的回調函數;如果有一個失敗或都失敗了,就執行fail()指定的回調函數。
四、普通操作的回調函數接口
$(function(){ var A = function(){ //並不是ajax var dtd = $.Deferred(); setTimeout(function(){ dtd.resolve("買好菜了"); }, 1000); return dtd.promise(); // 返回promise對象 }; var B = function(){ var dtd = $.Deferred(); setTimeout(function(){ dtd.resolve("買好肉了"); }, 2000); return dtd.promise(); // 返回promise對象 }; var C = function(){ var dtd = $.Deferred(); setTimeout(function(){ dtd.resolve("洗米好了"); }, 2000); return dtd.promise(); // 返回promise對象 }; $.when(A(), B(), C()) .done(function(v1, v2, v3){ console.log("煮飯啦~~~", v1 , v2, v3); }) .fail(function(){ alert("出錯啦!"); }); });
PS:deferred.promise()方法。它的作用是,在原來的deferred對象上返回另一個deferred對象,后者只開放與改變執行狀態無關的方法(比如done()方法和fail()方法),屏蔽與改變執行狀態有關的方法(比如resolve()方法和reject()方法),從而使得執行狀態不能被改變。
總結
發布/訂閱模式相對算是一種較為原始的方式,Promise/Deferred模式貢獻了一個非常不錯的異步任務模型的抽象,而實際上我們可能會常用的是第三方庫來做異步編程,畢竟這些比較成熟及方便。另外如果有其他更好的異步編程方式請回復我一下,讓我可以進行總結。
參考文獻
《jQuery的deferred對象詳解》 by 阮一峰