JavaScript: 如何编写一个既支持Callback又支持Promise风格的函数



Node.js 发布之初,对于异步操作,都是以 error first callback 风格提供执行结果。

fs.readFile('/foo.txt', (err, data) => {
  // err非空时,表示出现异常
  // data是操作的结果,这里是读取文件的内容
  console.log(data);
});

对于简单的任务还好,如果一项任务涉及多个异步操作,容易出现代码多层嵌套;而并行的异步操作协同起来也不方便。

JavaScript: 如何编写一个既支持Callback又支持Promise风格的函数
Callback Hell

于是,Promise 被引入了。为了方便,Node官方的fs包,甚至提供了promise版本的各项函数:

fs.promises
  .readFile("t.js")
  .then((text) => {})
  .catch((err) => {});

对于其它的已经存在的 error first callback 函数,Node 也提供了util.promisify函数,以帮助转换:

const util = require("util");

util.promisify(fs.readFile);

以上是 callback风格Promise风格 的一点简单历史。再回到主题上来:

如何让一个函数同时支持 Callback 和 Promise 风格?


先说说我们要的效果,它是这样的:

// 一个异步函数
function someFun(callback{}

// 可以传callback执行
someFun(console.log);

// 可以返回Promise
someFun().then(console.log);

假设这个异步函数是在2秒后生成一个随机数。对于 callback 风格,可以这样写:

function someFun(callback{
  setTimeout(() => {
    callback(null,Math.random());
  }, 2000);
}

对于 Promise 风格,可以这样写:

function someFun({
  return new Promise((res) => {
    setTimeout(() => {
      res(Math.random());
    }, 2000);
  });
}

怎么将2种风格合并呢?


最初的思考是,判断 callback参数是否存在,不存在则返回 Promise,否则调用 callback

function someFun(callback{
  if (callback === undefined) {
    return new Promise((res) => {
      setTimeout(() => {
        res(Math.random());
      }, 2000);
    });
  }

  setTimeout(() => {
    callback(nullMath.random());
  }, 2000);
}

虽然运行起来没问题,但让人感觉别扭,因为 延时生成随机数 这块代码,重复了2次。

再一想,可以优化如下:

function someFun(callback{
  if (callback === undefined) {
    return new Promise((res, rej) => {
      someFun((err, data) => (err ? rej(err) : res(data)));
    });
  }

  setTimeout(() => {
    callback(nullMath.random());
  }, 2000);
}

Promise 复用 callback 的逻辑,达到了消除重复代码的问题。

不考虑具体功能,在编写自己的同时支持两种调用风格的代码时,得用以上模板会有问题吗?

请思考一会再往后读。


以上代码的问题

如果单纯按 延时生成随机数 这个要求,上述代码无可厚非。但我们写文章通常是为了要泛化(generalize)某个思考方式,使其具有普适性(更广的用途),而不是局限于某个特定的单一问题。

上述代码有问题吗?如果我们的异步函数内部使用了 this,代码便会出现问题。

提供一个检验的方式供参考:将异步函数someFun挂载到某个对象obj下,然后以 promise 风格执行:

// 一个异步函数
function someFun(callback) {
  console.log(this);
  if (callback === undefined) {
    return new Promise((res, rej) => {
      someFun((err, data) => (err ? rej(err) : res(data)));
    });
  }

  setTimeout(() => {
    callback(null, Math.random());
  }, 2000);
}

const obj = {
  someFun,
};

// 以 Promise 风格执行
obj.someFun().then(console.log);

你猜会打印什么?

控制台运行结果如下:

JavaScript: 如何编写一个既支持Callback又支持Promise风格的函数

惊讶吗?

首先,打印了2次,因为callback参数为空时,我们会以callback方式重新执行someFun函数。

更重要的问题是——2次打印的this竟然不同!

对于同时支持两种风格的函数,如果其依赖了this(比如使用了this上的属性或方法),某些情况下你会得到意想不到的结果!

怎么解决?还记得 JavaScript 中每个函数都有callapply方法吗?在我看来,它们都是函数的瑰宝(另外还有bind),在解决this问题上,总能帮上忙。

重整后的实现如下:

function someFun(callback) {
  console.log(this);
  if (callback === undefined) {
    return new Promise((res, rej) => {
      someFun.call(this, (err, data) => (err ? rej(err) : res(data)));
    });
  }

  setTimeout(() => {
    callback(null, Math.random());
  }, 2000);
}

注意 someFun.call(this,...) 的使用,它将 this 绑定为函数初始执行时的值(本例中即obj)。再运行上面的例子,我们得到:

JavaScript: 如何编写一个既支持Callback又支持Promise风格的函数

可以看到,2次打印的this是一致的,即使你用到this上其它的属性和方法,也不会有问题了。


本文灵感来自于 Github.com/nodejs/undici[1]nodejs/undici 是 Node.js 官方维护的一个 http client 库,没用过的,推荐大家尝尝鲜。

参考资料

[1]

github.com/nodejs/undici: https://github.com/nodejs/undici/blob/master/lib/client-request.js

– END –


原文始发于微信公众号(背井):JavaScript: 如何编写一个既支持Callback又支持Promise风格的函数

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246852.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!