原文點擊這裏
在js世界裏,咱們衆所周知的惡魔,或許沒有那麼可怕,咱們是否是多了一些誤解?promise
我不會對術語回調地獄挖的太深,僅僅只是經過這篇文章解釋一些問題和典型的解決方案。若是你對這個術語還不太熟悉,能夠先去看看其餘的文章。我會一直在這等你回來!
Ok,我先複製粘貼一下問題代碼,而後,讓咱們一塊兒用回調函數來解決,而不是採用promise/async/await
。bash
const verifyUser = function(username, password, callback) {
dataBase.verifyUser(username, password, (error, userInfo) => {
if (error) {
callback(error);
} else {
dataBase.getRoles(username, (error, roles) => {
if (error) {
callback(error);
} else {
dataBase.logAccess(username, error => {
if (error) {
callback(error);
} else {
callback(null, userInfo, roles);
}
});
}
});
}
});
};
複製代碼
觀察代碼,你會發現,每次須要執行異步操做時,必須傳遞一個回調函數來接收異步的結果。 因爲咱們線性且匿名定義了全部的回調函數,導致它成爲一個自下而上,層層危險疊加的回調函數金字塔(實際過程當中,這種嵌套可能會更多,更深,更復雜)。
第一步,咱們先簡單重構一下代碼:將每一個匿名函數賦值給獨立的變量。引入柯里化參數(curried aruguments)來繞過環境做用域中的變量。異步
const verifyUser = (username, password, callback) =>
dataBase.verifyUser(username, password, f(username, callback));
const f = (username, callback) => (error, userInfo) => {
if (error) {
callback(error);
} else {
dataBase.getRoles(username, g(username, userInfo, callback));
}
};
const g = (username, userInfo, callback) => (error, roles) => {
if (error) {
callback(error);
} else {
dataBase.logAccess(username, h(userInfo, roles, callback));
}
};
const h = (userInfo, roles, callback) => (error, _) => {
if (error) {
callback(error);
} else {
callback(null, userInfo, roles);
}
};
複製代碼
若是沒點其餘東西的話,確定有點吹捧的意思。可是這些代碼仍然有如下的問題:async
if (error) { ... } else { ... }
模式重複使用;verifyUser
、f
、g
和h
相互高度耦合,由於他們互相引用。在咱們處理任何這些問題以前,讓咱們注意這些表達式之間的一些類似之處:
全部這些函數都接受一些數據和callback
參數。f,g而且h另外接受一對參數(error, something)
,其中只有一個將是一個非null/ undefined
值。若是error
不爲null
,該函數當即拋給callback並終止。不然,something
會被執行來作更多的工做,最終致使callback
接收到不一樣的錯誤,或者null和一些結果值。
腦海中記住這些共性,咱們將開始重構中間表達式,使它們看起來愈來愈類似。函數
我發現if
語句很累贅,因此咱們花點時間用三元表達式來代替。因爲返回值被丟棄,如下代碼不會有任何的行爲。工具
const f = (username, callback) => (error, userInfo) =>
error
? callback(error)
: dataBase.getRoles(username, g(username, userInfo, callback));
const g = (username, userInfo, callback) => (error, roles) =>
error
? callback(error)
: dataBase.logAccess(username, h(userInfo, roles, callback));
const h = (userInfo, roles, callback) => (error, _) =>
error ? callback(error) : callback(null, userInfo, roles);
複製代碼
由於咱們即將開始用函數參數進行一些嚴肅的操做,因此我將藉此機會儘量的柯里化函數。
咱們不能柯里化(error,xyz)
參數,由於databese
API指望回調函數攜帶兩個參數,可是咱們能夠柯里化其餘參數。咱們後面將圍繞dataBase
API 使用如下柯里化包裝器:ui
const dbVerifyUser = username => password => callback =>
dataBase.verifyUser(username, password, callback);
const dbGetRoles = username => callback =>
dataBase.getRoles(username, callback);
const dbLogAccess = username => callback =>
dataBase.logAccess(username, callback);
複製代碼
另外,咱們替換callback(null, userInfo, roles)
爲callback(null, { userInfo, roles })
,以便於除了不可避免的error
參數以外咱們只處理一個參數便可。spa
const verifyUser = username => password => callback =>
dbVerifyUser(username)(password)(f(username)(callback));
const f = username => callback => (error, userInfo) =>
error
? callback(error)
: dbGetRoles(username)(g(username)(userInfo)(callback));
const g = username => userInfo => callback => (error, roles) =>
error ? callback(error) : dbLogAccess(username)(h(userInfo)(roles)(callback));
const h = userInfo => roles => callback => (error, _) =>
error ? callback(error) : callback(null, { userInfo, roles });
複製代碼
讓咱們多作一些重構。咱們將把全部錯誤檢查代碼「向外」拉出一個級別,代碼就會暫時變得清晰。咱們將使用一個接收當前步驟的錯誤或結果的匿名函數,而不是每一個步驟都執行本身的錯誤檢查,若是沒有問題,則將結果和回調轉發到下一步:線程
const verifyUser = username => password => callback =>
dbVerifyUser(username)(password)((error, userInfo) =>
error ? callback(error) : f(username)(callback)(userInfo)
);
const f = username => callback => userInfo =>
dbGetRoles(username)((error, roles) =>
error ? callback(error) : g(username)(userInfo)(callback)(roles)
);
const g = username => userInfo => callback => roles =>
dbLogAccess(username)((error, _) =>
error ? callback(error) : h(userInfo)(roles)(callback)
);
const h = userInfo => roles => callback => callback(null, { userInfo, roles });
複製代碼
注意錯誤處理如何徹底從咱們的最終函數中消失:h
。它只接受幾個參數而後當即將它們輸入到它接收的回調中。
callback
參數如今在各個位置傳遞,所以爲了保持一致性,咱們將移動參數,以便全部數據首先出現而且callback最後出現:code
const verifyUser = username => password => callback =>
dbVerifyUser(username)(password)((error, userInfo) =>
error ? callback(error) : f(username)(userInfo)(callback)
);
const f = username => userInfo => callback =>
dbGetRoles(username)((error, roles) =>
error ? callback(error) : g(username)(userInfo)(roles)(callback)
);
const g = username => userInfo => roles => callback =>
dbLogAccess(username)((error, _) =>
error ? callback(error) : h(userInfo)(roles)(callback)
);
const h = userInfo => roles => callback => callback(null, { userInfo, roles });
複製代碼
到目前爲止,您可能已經開始在混亂中看到一些模式。特別是callback經過計算進行錯誤檢查和線程處理的代碼很是重複,可使用如下兩個函數進行分解:
const after = task => next => callback =>
task((error, v) => (error ? callback(error) : next(v)(callback)));
const succeed = v => callback => callback(null, v);
複製代碼
咱們的步驟變成:
const verifyUser = username => password =>
after(dbVerifyUser(username)(password))(f(username));
const f = username => userInfo =>
after(dbGetRoles(username))(g(username)(userInfo));
const g = username => userInfo => roles =>
after(dbLogAccess(username))(_ => h(userInfo)(roles));
const h = userInfo => roles => succeed({ userInfo, roles });
複製代碼
是時候停一下了,嘗試將after
和suceed
內聯入這些新的表達式中。這些新表達確實等同於咱們考慮的因素。
OK,看一下,f
、g
和h
看起來已經沒什麼用了呢!
······因此,讓咱們甩了它們!咱們所要作的就是從h向後,將每一個函數內聯到引用它的定義中:
// 內聯 h 到 g 中
const g = username => userInfo => roles =>
after(dbLogAccess(username))(_ => succeed({ userInfo, roles }));
複製代碼
// 內聯 g 到 f
const f = username => userInfo =>
after(dbGetRoles(username))(roles =>
after(dbLogAccess(username))(_ => succeed({ userInfo, roles }))
);
複製代碼
// 內聯 f 到 verifyUser
const verifyUser = username => password =>
after(dbVerifyUser(username)(password))(userInfo =>
after(dbGetRoles(username))(roles =>
after(dbLogAccess(username))(_ => succeed({ userInfo, roles }))
)
);
複製代碼
咱們可使用引用透明度來引入一些臨時變量並使其更具可讀性:
const verifyUser = username => password => {
const userVerification = dbVerifyUser(username)(password);
const rolesRetrieval = dbGetRoles(username);
const logEntry = dbLogAccess(username);
return after(userVerification)(userInfo =>
after(rolesRetrieval)(roles =>
after(logEntry)(_ => succeed({ userInfo, roles }))
)
);
};
複製代碼
如今你已經獲得了!它至關簡潔,沒有任何重複的錯誤檢查,甚至和promise
模式有點類似。你會像這樣調用verifyUser
:
const main = verifyUser("someusername")("somepassword");
main((e, o) => (e ? console.error(e) : console.log(o)));
複製代碼
// callback測序工具APIs
const after = task => next => callback =>
task((error, v) => (error ? callback(error) : next(v)(callback)));
const succeed = v => callback => callback(null, v);
// 柯里化後的database Api
const dbVerifyUser = username => password => callback =>
dataBase.verifyUser(username, password, callback);
const dbGetRoles = username => callback =>
dataBase.getRoles(username, callback);
const dbLogAccess = username => callback =>
dataBase.logAccess(username, callback);
// 成果
const verifyUser = username => password => {
const userVerification = dbVerifyUser(username)(password);
const rolesRetrieval = dbGetRoles(username);
const logEntry = dbLogAccess(username);
return after(userVerification)(userInfo =>
after(rolesRetrieval)(roles =>
after(logEntry)(_ => succeed({ userInfo, roles }))
)
);
};
複製代碼
咱們完成了嗎?有些人可能仍然以爲verifyUser
的定義有點過於三角化。有辦法解決,可是首先咱們作點其餘的事。
我沒有獨立發現重構此代碼時定義after
和succeed
過程。我實際上預先定義了這些定義,由於我從Haskell庫中複製了它們,它們的名稱爲>>=
和pure
。這兩個函數共同構成了"continuation monad"
(譯者注:能夠理解爲把嵌套式的金字塔結構打平變成鏈式結構能力的一種模式)的定義。
讓咱們以不一樣的方式格式化定義verifyUser
:
const verifyUser = username => password => {
const userVerification = dbVerifyUser(username)(password);
const rolesRetrieval = dbGetRoles(username);
const logEntry = dbLogAccess(username);
// prettier-ignore
return after (userVerification) (userInfo =>
after (rolesRetrieval) (roles =>
after (logEntry) (_ =>
succeed ({ userInfo, roles }) )));
};
複製代碼
更換succeed
和after
與那些奇怪的別名:
const M = { ">>=": after, pure: succeed };
const verifyUser = username => password => {
const userVerification = dbVerifyUser(username)(password);
const rolesRetrieval = dbGetRoles(username);
const logEntry = dbLogAccess(username);
return M[">>="] (userVerification) (userInfo =>
M[">>="] (rolesRetrieval) (roles =>
M[">>="] (logEntry) (_ =>
M.pure ({ userInfo, roles }) )));
};
複製代碼
M
是咱們對"continuation monad"
的定義,具備錯誤處理和不純的反作用。這裏省略了細節以防止文章變長兩倍,可是這種相關性是有許多方便的方法來排序不受金字塔末日效應影響的單子計算("continuation monad"
)。沒有進一步的解釋,這裏有幾種表達方式verifyUser
:
const { mdo } = require("@masaeedu/do");
const verifyUser = username => password =>
mdo(M)(({ userInfo, roles }) => [
[userInfo, () => dbVerifyUser(username)(password)],
[roles, () => dbGetRoles(username)],
() => dbLogAccess(username),
() => M.pure({ userInfo, roles })
]);
複製代碼
//適用提高
const verifyUser = username => password =>
M.lift(userInfo => roles => _ => ({ userInfo, roles }))([
dbVerifyUser(username)(password),
dbGetRoles(username),
dbLogAccess(username)
]);
複製代碼
我故意避免在這篇文章的大部份內容中引入類型簽名或monad
這樣的概念,以使事情變得平易近人。也許在將來的帖子中,咱們能夠用咱們頭腦中最重要的monad
和monad-transformer
概念從新推導出這種抽象,並特別注意類型和規律。
很是感謝@jlavelle,@mvaldesdeleon和@gabejohnson提供有關此帖子的反饋和建議。