原文点击这里
在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提供有关此帖子的反馈和建议。