原文点击这里
在 JS 世界里, 我们众所周知的恶魔, 或许没有那么可怕, 我们是不是多了一些误解?
走进回调地狱
我不会对术语回调地狱挖的太深, 仅仅只是通过这篇文章解释一些问题和典型的解决方案. 如果你对这个术语还不太熟悉, 可以先去看看其他的文章. 我会一直在这等你回来!
Ok, 我先复制粘贴一下问题代码, 然后, 让我们一起用回调函数来解决, 而不是采用 promise/async/await.
- 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);
- }
- };
如果没点其他东西的话, 肯定有点吹捧的意思. 但是这些代码仍然有以下的问题:
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) 参数, 因为 databeseAPI 期望回调函数携带两个参数, 但是我们可以柯里化其他参数. 我们后面将围绕 dataBaseAPI 使用以下柯里化包装器:
- 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 参数之外我们只处理一个参数即可.
- 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 最后出现:
- 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 概念重新推导出这种抽象, 并特别注意类型和规律.
来源: https://juejin.im/post/5c05f3ba5188250f3e1d3663