既然我们有了 ES6 承诺,还有理由使用像 Q 或 BlueBird 这样的承诺库吗?

Are there still reasons to use promise libraries like Q or BlueBird now that we have ES6 promises?

在 Node.js 添加了对 promises 的本地支持后,是否还有理由使用 Q 或 BlueBird 等库?

例如,如果您正在开始一个新项目,假设您在这个项目中没有任何使用这些库的依赖项,我们可以说真的没有更多理由使用这些库吗?

俗话说,你应该为工作选择合适的工具。 ES6 承诺提供基础知识。如果您想要或需要的只是基础知识,那么 should/could 非常适合您。但是,工具箱中的工具不仅仅是基础工具,而且在某些情况下,这些额外的工具非常有用。而且,我认为 ES6 promises 甚至缺少一些基础知识,例如 promisification,它们在几乎每个 node.js 项目中都很有用。

我对 Bluebird promise library 最熟悉,所以我将主要根据我使用该库的经验来发言。

所以,这是我使用功能更强大的 Promise 库的 6 大理由

  1. Non-Promisified 异步接口 - .promisify().promisifyAll() 对于处理所有那些仍然存在的异步接口非常有用需要简单的回调并且还没有 return 承诺 - 一行代码创建整个界面的承诺版本。

  2. 更快 - 在大多数环境中,Bluebird 比本机 promise significantly faster

  3. 异步数组迭代的排序 - Promise.mapSeries()Promise.reduce() 允许您遍历数组,调用异步操作每个元素,但对异步操作进行排序,以便它们一个接一个地发生,而不是同时发生。您可以这样做,因为目标服务器需要它,或者因为您需要将一个结果传递给下一个。

  4. Polyfill - 如果你想在旧版本的浏览器客户端中使用 promises,无论如何你都需要一个 polyfill。还不如得到一个有能力的 polyfill。由于 node.js 具有 ES6 承诺,因此您不需要 node.js 中的 polyfill,但您可以在浏览器中使用。如果您同时对 node.js 服务器和客户端进行编码,则在两者中拥有相同的承诺库和功能可能非常有用(更容易共享代码、环境之间的上下文切换、对异步代码使用通用编码技术等) ...).

  5. 其他有用的功能 - Bluebird 具有 Promise.map()Promise.some()Promise.any()Promise.filter()Promise.each()Promise.props() 所有这些偶尔都很方便。虽然这些操作可以使用 ES6 承诺和附加代码执行,但 Bluebird 已经带有这些操作 pre-built 和 pre-tested,因此使用它们更简单,代码更少。

  6. 内置警告和完整堆栈跟踪 - Bluebird 有许多内置警告,提醒您注意可能是错误代码或错误的问题。例如,如果您调用一个在 .then() 处理程序中创建新承诺的函数,而没有 return 该承诺(将其 link 放入当前承诺链),那么在大多数情况下,这是一个意外错误,Bluebird 会就此向您发出警告。其他 built-in 蓝鸟警告是 described here.

以下是关于这些不同主题的更多详细信息:

PromisifyAll

在任何 node.js 项目中,我都会立即在任何地方使用 Bluebird,因为我在 fs 模块等标准 node.js 模块上大量使用 .promisifyAll()

Node.js 本身并不像 fs 模块那样为执行异步 IO 的 built-in 模块提供承诺接口。所以,如果你想对这些接口使用 promises,你要么在你使用的每个模块函数周围手工编写一个 promise 包装器,要么获得一个可以为你做这件事的库,或者不使用 promises。

Bluebird 的 Promise.promisify()Promise.promisifyAll() 提供了 node.js 调用约定异步 API 到 return 承诺的自动包装。它非常有用并且可以节省时间。我一直在用它。

这是一个工作原理示例:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

另一种方法是为每个要使用的 fs API 手动创建自己的承诺包装器:

const fs = require('fs');

function readFileAsync(file, options) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, options, function(err, data) {
            if (err) {
                reject(err);
            } else {
                 resolve(data);
            }
        });
    });
}

readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

而且,您必须为要使用的每个 API 功能手动执行此操作。这显然没有道理。这是样板代码。您不妨获得一个实用程序来为您完成这项工作。 Bluebird 的 Promise.promisify()Promise.promisifyAll() 就是这样一个实用程序。

其他有用的功能

以下是我特别认为有用的一些 Bluebird 功能(下面有几个代码示例说明这些功能如何节省代码或加快开发速度):

Promise.promisify()
Promise.promisifyAll()
Promise.map()
Promise.reduce()
Promise.mapSeries()
Promise.delay()

除了有用的功能外,Promise.map()还支持并发选项,让您可以指定允许同时执行多少个操作运行,这在您有有很多事情要做,但不能淹没一些外部资源。

其中一些可以同时调用 stand-alone 并用于本身解析为可节省大量代码的可迭代对象的承诺。


填充

在浏览器项目中,由于您通常希望仍然支持一些不支持 Promise 的浏览器,因此您最终还是需要一个 polyfill。如果你也在使用jQuery,有时你可以只使用 jQuery 中内置的 promise 支持(尽管在某些方面 non-standard 很痛苦,也许在 jQuery 3.0 中修复了),但是如果项目涉及任何重要的异步 activity,我发现 Bluebird 中的扩展功能非常有用。


更快

还值得注意的是,Bluebird 的承诺似乎比 V8 内置的承诺要快得多。有关该主题的更多讨论,请参阅 this post


一件大东西 Node.js 不见了

让我考虑在 node.js 开发中少用 Bluebird 的是,如果 node.js 内置了一个 promisify 函数,那么你可以这样做:

const fs = requirep('fs');

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

或者只是提供已经承诺的方法作为 built-in 模块的一部分。

在那之前,我用 Bluebird 做这个:

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));

fs.readFileAsync('somefile.text').then(function(data) {
   // do something with data here
});

在 node.js 中内置 ES6 promise 支持并具有 built-in 模块 none return promises 似乎有点奇怪。这需要在 node.js 中解决。在那之前,我使用 Bluebird 来 promisify 整个库。因此,自从 built-in 模块的 none 允许您使用 promises 而无需先手动包装它们,现在感觉 promise 在 node.js 中实现了大约 20%。


例子

这是一个简单的 Promises 与 Bluebird 的 promisify 和 Promise.map() 的示例,用于并行读取一组文件并在完成所有数据时发出通知:

平淡的承诺

const files = ["file1.txt", "fileA.txt", "fileB.txt"];
const fs = require('fs');

// make promise version of fs.readFile()
function fsReadFileP(file, options) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, options, function(err, data) {
            if (err) return reject(err);
            resolve(data);
        });
    });
}


Promise.all(files.map(fsReadFileP)).then(function(results) {
    // files data in results Array
}, function(err) {
    // error here
});

蓝鸟 Promise.map()Promise.promisifyAll()

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
const files = ["file1.txt", "fileA.txt", "fileB.txt"];

Promise.map(files, fs.readFileAsync).then(function(results) {
    // files data in results Array
}, function(err) {
    // error here
});

这是一个简单的 Promises 与 Bluebird 的 promisify 和 Promise.map() 从远程主机读取一堆 URL 的示例,您一次最多可以读取 4 个,但希望在其中保留尽可能多的请求允许平行:

普通 JS Promises

const request = require('request');
const urls = [url1, url2, url3, url4, url5, ....];

// make promisified version of request.get()
function requestGetP(url) {
    return new Promise(function(resolve, reject) {
        request.get(url, function(err, data) {
            if (err) return reject(err);
            resolve(data);
        });
    });
}

function getURLs(urlArray, concurrentLimit) {
    var numInFlight = 0;
    var index = 0;
    var results = new Array(urlArray.length);
    return new Promise(function(resolve, reject) {
        function next() {
            // load more until concurrentLimit is reached or until we got to the last one
            while (numInFlight < concurrentLimit && index < urlArray.length) {
                (function(i) {
                    requestGetP(urlArray[index++]).then(function(data) {
                        --numInFlight;
                        results[i] = data;
                        next();
                    }, function(err) {
                        reject(err);
                    });
                    ++numInFlight;
                })(index);
            }
            // since we always call next() upon completion of a request, we can test here
            // to see if there was nothing left to do or finish
            if (numInFlight === 0 && index === urlArray.length) {
                resolve(results);
            }
        }
        next();
    });
}

蓝鸟承诺

const Promise = require('bluebird');
const request = Promise.promisifyAll(require('request'));
const urls = [url1, url2, url3, url4, url5, ....];

Promise.map(urls, request.getAsync, {concurrency: 4}).then(function(results) {
    // urls fetched in order in results Array
}, function(err) {
    // error here
});