事实上,回调函数还不错!!

栏目: 数据库 · 发布时间: 6年前

内容简介:在js世界里,我们众所周知的恶魔,或许没有那么可怕,我们是不是多了一些误解?我不会对术语回调地狱挖的太深,仅仅只是通过这篇文章解释一些问题和典型的解决方案。如果你对这个术语还不太熟悉,可以先去看看其他的文章。我会一直在这等你回来!

原文点击这里

在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);
  }
};
复制代码

如果没点其他东西的话,肯定有点吹捧的意思。但是这些代码仍然有以下的问题:

  1. if (error) { ... } else { ... } 模式重复使用;
  2. 变量名字对逻辑毫无意义;
  3. verifyUserfgh 相互高度耦合,因为他们互相引用。

看看这种模式

在我们处理任何这些问题之前,让我们注意这些表达式之间的一些相似之处:

所有这些函数都接受一些数据和 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 使用以下柯里化包装器:

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 });
复制代码

是时候停一下了,尝试将 aftersuceed 内联入这些新的表达式中。这些新表达确实等同于我们考虑的因素。

OK,看一下, fgh 看起来已经没什么用了呢!

减负

······所以,让我们甩了它们!我们所要做的就是从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 的定义有点过于三角化。有办法解决,但是首先我们做点其他的事。

我没有独立发现重构此代码时定义 aftersucceed 过程。我实际上预先定义了这些定义,因为我从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 }) )));
};
复制代码

更换 succeedafter 与那些奇怪的别名:

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 这样的概念,以使事情变得平易近人。也许在未来的帖子中,我们可以用我们头脑中最重要的 monadmonad-transformer 概念重新推导出这种抽象,并特别注意类型和规律。

致谢

非常感谢@jlavelle,@mvaldesdeleon和@gabejohnson提供有关此帖子的反馈和建议。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Lean Analytics

Lean Analytics

Alistair Croll、Benjamin Yoskovitz / O'Reilly Media / 2013-3-18 / USD 29.99

If you're involved with a startup, analytics help you find your way to the right product and market before the money runs out. But with a flood of information available, where do you start? This book ......一起来看看 《Lean Analytics》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换