非同步處理方案系列- 1.callback

hooper發表於2018-12-14

原創禁止私自轉載


非同步處理方案系列- 1.callback

引言

非同步/非同步操作,已經是前端領域一個老生常談的話題.也是做前端開發中經常面臨的一個問題.

然而非同步的問題往往比較複雜且難於處理, 特別是非同步問題還經常不是單獨出現,往往存在比較多樣的組合關係.

在實際處理中就顯得更加複雜而難於處理. 特別是在 io 操作頻繁,或者 node server 中,經常遇到非常複雜的組合型非同步。

舉個業務開發中常見的例子:

eg: 省市縣三級級聯問題

這個問題非常常見, 假設資料量較大, 我們大多數情況下不會一次載入所有的資料, 然後做前端級聯的方案.

而是採取三個資料介面,在下拉改變的時候去動態請求的方式.這就形成一種很常見的多個非同步序列的模型.

怎麼處理這樣的問題, 怎麼較好的維護多個非同步之間的關係, 怎麼讓程式碼正常執行的同時,在邏輯和結構上更可讀呢?

我將會梳理

  • callback
  • cps
  • thunk
  • defer / promise(非 es6)
  • promise(ES6)
  • generator ->
    co.
  • async / await

這幾種處理方式. 加上兩種模式

  • 事件監聽
  • 訂閱釋出模型

列出一個系列的部落格去討論這個問題.看我們在不同階段, 使用不同技術,如何處理相同的問題. 在不同方案之間橫向對比, 去深入瞭解技術變遷以及背後的處理思路和邏輯的變化.

callback

什麼是回撥呢? 這麼問似乎有點多餘, 每個寫過 javascript 的開發者, 或多或少都會接觸到回撥. 回撥的使用成本很低,實現回撥函式就像傳遞一般的引數變數一樣簡單.由於函數語言程式設計極好的支援,以至於這項技術使用基本沒有障礙.我們隨手就能寫出一個回撥

Ajax.get('http://xxxx', {
}, (resp) =>
{
// .....
})複製程式碼

但是呢,要真給回撥下一個定義, 也還真不好回答.

我們不妨從一些側面去看看回撥

  • 回撥是一種處理特定問題的模式, 伴隨著函數語言程式設計而生. 函數語言程式設計中很重要的技術之一就是回撥函式
  • 當一個函式作為主調函式的引數時, 它往往會在特定的時間和場景(上下文)中執行.
  • 執行過程中,回撥函式選擇性接收函式內部的資料, 或者狀態(記憶體), 經過處理選擇性返回,或者改變狀態(hock).

callback 業務模型

說這麼多, 我們不如從程式碼的角度去解決一個序列的非同步模型.

為了說明問題, 我們將問題簡化成 A B C 三個非同步(可能是 io, 網路請求, 或者其他.為了方面描述, 我們採用 settimeout 來模擬), 這三個非同步耗時不確定, 但是必須按照 A B C 的順序處理他們的返回結果.

處理這個問題, 我們基本上有兩種思路:

  1. 控制非同步發出的順序, 在 a 返回之後再發 b 請求, 這樣將問題序列化(省市縣模型中經常需要省的返回值去請求省所對應的市).
  2. 同時發出非同步請求,控制處理的順序.

方案一: 序列化請求

// 模擬 ajax 函式function ajax(url) { 
return function (cb) {
setTimeout(function() {
cb({
url
});

}, Math.random() * 3000);

}
}// 初始化出三個請求const A = ajax('/ofo/a');
const B = ajax('/ofo/b');
const C = ajax('/ofo/c');
// 控制請求順序log('ajax A send...');
A(function (a) {
log('ajax A receive...');
log('ajax B send...');
B(function (b) {
log('ajax B receive...');
log('ajax C send...');
C(function (C) {
log('ajax C receive...');

});

})
})複製程式碼

程式碼很簡單, 大多是方案也是這麼走的, 因為 A 的返回值可以作為 B 的引數.但是相應的這個模式的總時間必定大於三個請求的時間之和.輸出如下:

ajax A send...ajax A receive...ajax B send...ajax B receive...ajax C send...ajax C receive...複製程式碼

方案二: 自由請求,序列化處理

是相對不那麼通用的方案, 但是處理沒有直接資料依賴的序列請求非常合適.

// 傳送容器const sender = [];
// 稍作改造function ajax(url, time) {
return function(cb) {
// 記錄傳送順序, 必須有序 sender.push(url);
setTimeout(function() {
const data = {
from: url, reso: 'ok'
};
// 將 data, 回撥傳遞給一個處理函式 dealReceive({url, cb, data
});

}, time);

}
}// 按照順序處理返回結果// 返回結果容器const receiver = {
};
function dealReceive({url, cb, data
}
)
{
// 記錄返回結果.可以無序 receiver[url] = {cb, data
};
for (var i = 0;
i <
sender.length;
i++) {
let operate = receiver[sender[i]];
if(typeof operate === 'object') {
operate.cb.call(null, operate.data);

} else {
return;

}
}
}// 手動模擬出請求時間, A 最耗時.b 最快, 更好說明問題const A = ajax('/ofo/a', 4000);
const B = ajax('/ofo/b', 600);
const C = ajax('/ofo/c', 2000);
// 注意我們的呼叫方式 是沒有任何控制的// A,B,C 依次發出. 還可以按照這個順序處理 A,B,C 的返回值A(function (a) {
log(a);

});
B(function (b) {
log(b);

});
C(function (c) {
log(c);

});
複製程式碼

輸出:

{"from":"/ofo/a","reso":"ok"
}{"from":"/ofo/b","reso":"ok"
}{"from":"/ofo/c","reso":"ok"
}複製程式碼

這種方案總耗時基本上是耗時最長的 ajax 的耗時。

值得注意的是, A,B,C 的呼叫上沒有做任何控制. A 最耗時, 但是要最最先處理 A 的返回資料.實現這一點的關鍵就在於我們 dealReceive 有個輪詢, 這個輪詢不是定時觸發的,而是每當請求回來時, 觸發輪詢. 整個過程輪詢 3 次.

基本上 callback 處理組合非同步模型的思路說完了.序列是容易處理的一種模型, 如果出現 c 依賴 a,b 都正確返回的模型時, 基本上我們暴力一點就是轉化為序列關係. 儘管 a, b 沒有關係.或者呢我們就在 a, b 的回撥裡做標誌位. 和 dealReceive 類似.

單個非同步不需要有太多處理, callback 的一些細節也不做討論. 主要討論是回撥在實際場景中的處理問題方案

回撥兩面性

我們還是落入俗套的分析一下回撥的優缺點.其實主要是缺點.

  • 優點: 使用成本低, 處理簡單問題非常方便.能夠拿到主調函式內部的環境.等等.
  • 大多數人認為的缺點:
  1. 回撥很 low: 可能是因為, 實現回撥函式就像傳遞一般的引數變數一樣簡單.由於函數語言程式設計極好的支援,以至於這項技術使用基本沒有障礙.也沒有比較嚴格的模式要求.大家習以為常了.
  2. 回撥地獄(程式碼橫向發展): 其實這並不是回撥的錯. 當我們遇到回撥無底洞的時候,也無需驚慌,其實這根本不是什麼問題, 因為同樣有協程和 monad 無底洞。因為如果你把任何一個抽象使用地足夠頻繁的話,都同樣會創造一個無底洞。

使用回撥上的建議: 沒有使用障礙導致回撥的濫用, 大部分問題都用了簡單的回撥堆疊來解決. 實際上我們有很多基於回撥的模式可以避免這些問題.比如: cps, cps 進一步轉化為 thunk.等等.

這樣看來, 回撥沒有缺點, 是這樣麼? 不是的. 回撥有非常致命的機制上的缺點, 這個問題可能在 node 中爆發,除非自身改變,或者被吃掉。

所謂的機制就是:你可能在用回撥處理複雜問題的時候,對自己能力產生懷疑,這些非同步之間的關係是那麼難以梳理清晰,而又難以寫出容易維護的程式碼.

其實這都不是你的錯.

  • 使用回撥處理非同步往往意味著,你捨棄了返回值,而使用回撥接收非同步操作結果. 而這正是用回撥風格來程式設計會很困難的根本原因: 回撥風格不返回任何值,所以難以組合[函數語言程式設計中函式有良好的輸入和輸出是函式可以組合的根本]。
  • 一個沒有返回值的函式執行的效果其實是利用它的副作用
  • 一個沒有返回值和利用副作用的函式其實就是一個黑洞。
  • 所以,使用回撥風格來程式設計無法避免會是指令式的,它實際上是通過把一系列嚴重依賴於副作用的操作安排好執行順序,而不是通過函式的呼叫來把輸入輸出值對應好。如果你是通過回撥組織程式執行流程, 而不是靠理順值的關係來解決問題的, 是很難編寫出正確的並行程式
  • 這種問題也間接的導致了回撥難於除錯,定位問題和維護.

最終的結果就是: 你崩潰了

注:系列部落格陸續推出,稍安勿躁。

來源:https://juejin.im/post/5c1395ef5188254707656a94

相關文章