一, 背景
1,Node.JS 异步控制
在之前写的 callback vs async.JS vs promise vs async / await 里, 我介绍了 ES6 的 promise 和 ES7 的 async / await 的基本用法.
可以肯定的是, node.JS 的异步控制 (asynchronous JavaScript),promise 就是未来的主流, 诸如 async.JS 等非 promise 库( async.JS 基于 callback ) 终将被淘汰, 而基于 promise 的第三方库 (Q,when,WinJS,RSVP.JS) 也会被 async / await 写法取代.
延伸阅读: 知乎 - Node.JS 异步控制「co,async,Q ,『es6 原生 promise』,then.JS,bluebird」有何优缺点? 最爱哪个? 哪个简单?
2, 已经有 ES6 Promise + async / await 了, 为什么还要用 bluebird ?
但目前基于 async / await 的 promise 写法还不是很强大. 这里可以考虑用 bluebird, 它是一个第三方的 Promise 库, 比 async / await 更早诞生, 但是完全兼容, 因为他们都是基于 Promises/A+ 的标准(下文会介绍).
很多第三方的 promise 库都是兼容 ES6 promise 的, 比如 Q .
二, Promise 进阶
1,Promise 前世今生
(1)定义
They describe an object that acts as a proxy for a result that is initially unknown, usually because the computation of its value is not yet complete.
有道翻译: 他们描述了一个对象, 该对象充当最初未知的结果的代理, 通常是因为其值的计算尚未完成.
"代理" 这个词用的挺好的.
(2)历史
promise 一词由丹尼尔. 福瑞得曼和 David Wise 在 1976 年提出.
后来演化出别称: future,delay 和 deferred, 通常可以互换使用.
promise 起源于函数式编程和相关范例 (如逻辑编程 ), 目的是将值(future) 与其计算方式 (promise) 分离, 从而允许更灵活地进行计算.
应用场景:
并行化计算
分布式计算
编写异步程序, 避免回调地狱
(3)各语言支持
现在主流的语言对 future/promise 都有支持.
Java 5 中的 FutureTask(2004 年公布)
.NET 4.5 中的 async / await
- Dart(2014)
- Python(2015)
- Hack(HHVM)
- ECMAScript 7(JavaScript)
- Scala
C++ 草案
- ......
- 2,Promises/A+
官方: https://promisesaplus.com/
介绍: An open standard for sound, interoperable JavaScript promises-by implementers, for implementers.
可以理解成 JavaScript 中 关于 promise 的实现标准.
3, 拓展 - jQuery 中的 Promise
(1)介绍
从 jQuery 1.5.0 版本开始引入的一个新功能 -- deferred 对象.
注意: Deferred 虽然也是一种 promise 的实现, 但是跟 Promise/A+ 并不兼容.
但可以将其转为标准的 promise, 例如:
var jsPromise = Promise.resolve($.Ajax('/whatever.json'))
(2)用法
因为 jQuery 现如今很少用到了, 仅简单介绍下 deferred 的用法吧.
1, 以 Ajax 操作为例:
$.Ajax() 操作完成后, 如果使用的是低于 1.5.0 版本的 jQuery, 返回的是 XHR 对象, 你没法进行链式操作; 如果高于 1.5.0 版本, 返回的是 deferred 对象, 可以进行链式操作.
- # old
- $.Ajax({
- url: "test.html",
- success: function(){
- alert("哈哈, 成功了!");
- },
- error:function(){
- alert("出错啦!");
- }
- });
- # new
- $.Ajax("test.html")
- .done(function(){ alert("哈哈, 成功了!"); })
- .fail(function(){ alert("出错啦!"); });
2, 其它
$.when() 类似 promise.all()
deferred.resolve(),deferred.reject() 类似 Promise.resolve(),Promise.reject()
......
三, bluebird
1, 介绍
英文文档:
http://bluebirdjs.com/docs/api-reference.html
中文文档:
https://itbilu.com/nodejs/npm/VJHw6ScNb.html
2, 安装
NPM install bluebird
3, 使用
const Promise = require('bluebird')
这样写会覆盖原生的 Promise 对象.
4, 早期原生性能问题
早期 JS 标准库里并没有包含 Promise, 所以被迫只能用第三方的 Promise 库, 例如 bluebird.
后来 ES6 和 ES7 相继推出了原生的 Promise 和 async/await , 但性能很差, 大家还习惯用例如 bluebird.
但到了 Node.JS v8.x , 原生性能已经得到了很大的优化, 可以不需要使用 bluebird 这样的第三方 Promise 库.(除非需要用到 bluebird 的更多 feature, 而原生是不具备的. 这个下面会详细介绍)
详情可以参考这篇文章: Node 8: 迎接 async await 新时代
四, bluebird 用法
这一章, 会结合 bluebird 用法 和 原生(主要以 ES7 的 async / wait) 探讨出最优写法.
1, 回调形式 -> Promise 形式
大部分 Node.JS 的标准库 API 和不少第三方库的 API 都使用了回调方法的模式, 也就是在执行异步操作时, 需要传入一个回调方法来接受操作的执行结果和可能出现的错误.
例如 Node.JS 的标准库中的 fs 模块:
- const fs = require('fs'),
- path = require('path');
- fs.readFile(path.join(__dirname, 'sample.txt'), 'utf-8', (err, data) => {
- if (err) {
- console.error(err);
- } else {
- console.log(data);
- }
- });
- (1)bluebird
对于这样的方法, bluebird 的 promisifyAll() 和 promisify() 可以很容易的将它们转换成使用 Promise 的形式.
- // 覆盖了原生的 Promise
- const Promise = require('bluebird'),
- fs = require('fs'),
- path = require('path');
- // 1,promisifyAll
- // Promise.promisifyAll 方法可以为一个对象的属性中的所有方法创建一个对应的使用 Promise 的版本
- Promise.promisifyAll(fs);
- // 这些新创建方法的名称在已有方法的名称后加上 "Async" 后缀
- // (除了 readFile 对应的 readFileAsync,fs 中的其他方法也都有了对应的 Async 版本, 如 writeFileAsync 和 fstatAsync 等)
- fs.readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8')
- .then(data => console.log(data))
- .catch(err => console.error(err));
- // 2,promisify
- // Promise.promisify 方法可以为单独的方法创建一个对应的使用 Promise 的版本
- let readFileAsync = Promise.promisify(fs.readFile)
- readFileAsync(path.join(__dirname, 'sample.txt'), 'utf-8')
- .then(data => console.log(data))
- .catch(err => console.error(err));
(2)原生
在 node.JS 8.x 版本中, 可以用 util.promisify() 实现 promisify() 一样的功能.
在官方推出这个工具之前, 民间已经有很多类似的工具了, 除了 bluebird.promisify, 还有比如 es6-promisify,thenify.
2, 使用 promise -- .finally()
.finally() 可以避免同样的语句需要在 then() 和 catch() 中各写一次的情况.
- (1)bluebird
- Promise.reject(new TypeError('some error'))
- .catch(TypeError, console.error)
- .finally(() => console.log('done'));
(2)自己实现
- Promise.prototype.finally = function (callback) {
- return this.then(function (value) {
- return Promise.resolve(callback()).then(function () {
- return value;
- });
- }, function (err) {
- return Promise.resolve(callback()).then(function () {
- throw err;
- });
- });
- };
- (3)async / await
用 try...catch...finally 的 finally 即可实现.
(4)原生
.finally() 是 ES2018(ES9)的新特性.
3, 使用 promise -- .cancel()
(1)bluebird
当一个 Promise 对象被 .cancel() 之后, 只是其回调方法都不会被调用, 并不会取消正在进行的异步操作.
- // 先修改全局配置, 让 promise 可被撤销
- Promise.config({
- cancellation: true, // 默认为 false
- });
- // 构造一个 promise 对象, 并设置 1000 ms 延迟
- let promise = Promise.resolve("hello").then((value) => {
- console.log("promise 的 async function 还是执行了......")
- return value
- }).delay(1000)
- // promise 对象上绑定回调函数
- promise.then(value => console.log(value))
- // 取消这个 promise 对象的回调
- setTimeout(() => {
- promise.cancel();
- }, 500);
输出:
promise 的 async function 还是执行了......
这里提到的 .delay() 方法下面会介绍.
(2)async / await
可以通过对 async / await 函数调用后的返回值, 做 if 判断, 决定要不要执行接下来的逻辑.
4, 处理 promise 集合
之前的代码示例都针对单个 Promise. 在实际中, 经常会处理与多个 Promise 的关系.
(1)bluebird
以 fs 模块分别读取 sample1.txt,sample2.txt,sample3.txt 三个文件的内容为例. 他们的文件内容分别为 "1","2","3".
- const Promise = require('bluebird'),
- fs = require('fs'),
- path = require('path');
- Promise.promisifyAll(fs);
- // 一, 并行操作
- // 1,Promise.all , 必须全部成功才通过 [保证返回顺序]
- Promise.all([
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8')
- ]).then(results => console.log(results.join(','))).catch(console.error);
- // 1.1,Promise.props , 约等于 Promise.all, 但不同的在于: 返回的不是数组而是对象 !
- Promise.props({
- app1: fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- app2: fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- app3: fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8'),
- }).then(results => console.log(results)).catch(console.error);
- // 1.2 Promise.join, 约等于 Promise.all [保证返回顺序] , 但不同的在于: 成功结果不是 array 而是多个参数 !
- Promise.join(
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8'),
- (a, b, c) => console.log(a, b, c));
- // 1.3,Promise.filter , 约等于 Promise.all 之后对成功结果的 Array 进行 filter 过滤 [保证返回顺序]
- Promise.filter([
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8')
- ], value => value> 1).then(results => console.log(results.join(','))).catch(console.error);
- // ----------
- // 2,Promise.map , 约等于 Promise.all [保证返回顺序]
- Promise.map(['sample1.txt', 'sample2.txt', 'sample3.txt'],
- name => fs.readFileAsync(path.join(__dirname, name), 'utf-8')
- ).then(results => console.log(results.join(','))).catch(console.error);
- // 2.1 Promise.reduce, 约等于 Promise.map
- Promise.reduce(['sample1.txt', 'sample2.txt', 'sample3.txt'],
- (total, name) => {
- return fs.readFileAsync(path.join(__dirname, name), 'utf-8').then(data => total + parseInt(data));
- }
- , 0).then(result => console.log(`Total size: ${result}`)).catch(console.error);
- // ----------
- // 3,Promise.some 只要成功 N 个就通过 [不保证返回顺序]
- Promise.some([
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8')
- ], 3).then(results => console.log(results.join(','))).catch(console.error);
- // 3.1,Promise.any 只要成功 1 个就通过, 约等于 Promise.some (N = 1), 但不同的在于: 返回的不是数组而是单个值了!
- Promise.any([
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8')
- ]).then(results => console.log(results)).catch(console.error);
- // 3.2,Promise.race 只要成功 1 个就通过, 约等于 Promise.any (N = 1), 但不同的在于: 如果成功返回前遇到了失败, 则会不通过!
- Promise.race([
- fs.readFileAsync(path.join(__dirname, 'sample1.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample2.txt'), 'utf-8'),
- fs.readFileAsync(path.join(__dirname, 'sample3.txt'), 'utf-8')
- ]).then(results => console.log(results)).catch(console.error);
- // ----------
- // 二, 串行
- // 4,Promise.mapSeries , 约等于 Promise.map [保证返回顺序] , 但不同的在于: 这是串行不是并行!
- Promise.mapSeries(['sample1.txt', 'sample2.txt', 'sample3.txt'],
- name => fs.readFileAsync(path.join(__dirname, name), 'utf-8').then(function(fileContents) {
- return name + "!";
- })
- ).then(results => console.log(results.join(','))).catch(console.error);
- // 'sample1.txt!, sample2.txt!, sample3.txt!'
- // 4.1,Promise.each , 约等于 Promise.mapSeries [保证返回顺序] , 但不同的在于: 只是单纯的遍历, 每次循环的 return 毫无影响 !
- Promise.each(['sample1.txt', 'sample2.txt', 'sample3.txt'],
- name => fs.readFileAsync(path.join(__dirname, name), 'utf-8').then(function(fileContents) {
- return name + "!"; // 无效
- })
- ).then(results => console.log(results.join(','))).catch(console.error);
- // 'sample1.txt, sample2.txt, sample3.txt'
1, 大多数函数都是并行的. 其中 map,filter 还有 Concurrency coordination (并发协调)功能.
注意:
1, 因为 Node.JS 是单线程, 这里的并发只是针对 promise 而言, 实际上底层还是串行.
2, 并发数的多少, 取决于你 promise 执行的具体功能, 如网络请求, 数据库连接等. 需根据实际情况来设置.
以 map 为例:
- // 控制并发数
- Promise.map(['sample1.txt', 'sample2.txt', 'sample3.txt'],
- name => fs.readFileAsync(path.join(__dirname, name), 'utf-8'),
- {concurrency: 2}
- ).then(results => console.log(results.join(','))).catch(console.error);
2,mapSeries,each 是串行, 也可以看成是 {concurrency: 1} 的特例.
(2)拓展 - promiseAll 实现原理
- function promiseAll(promises) {
- return new Promise(function(resolve, reject) {
- if (!isArray(promises)) {
- return reject(new TypeError('arguments must be an array'));
- }
- var resolvedCounter = 0;
- var promiseNum = promises.length;
- var resolvedValues = new Array(promiseNum);
- for (var i = 0; i <promiseNum; i++) {
- (function(i) {
- Promise.resolve(promises[i]).then(function(value) {
- resolvedCounter++
- resolvedValues[i] = value
- if (resolvedCounter == promiseNum) {
- return resolve(resolvedValues)
- }
- }, function(reason) {
- return reject(reason)
- })
- })(i)
- }
- })
- }
注意: Promise.resolve(promises[i])这段的意思, 是防止 promises[i] 为非 promise 对象, 而强制转成 promise 对象.
此源码地址为: https://www.npmjs.com/package/promise-all-simple
(3)async / await
对于上面的并行操作, 建议用 bluebird (原生貌似现在只支持 Promise.all() , 太少了).
对于上面的串行操作, 可以用 循环 搭配 async / await 即可.
5, 资源使用与释放
如果在 Promise 中使用了需要释放的资源, 如数据库连接, 我们需要确保这些资源被应有的释放.
(1)bluebird
方法 1:finally() 中添加资源释放的代码(上文有介绍)
方法 2[推荐] : 使用资源释放器 (disposer) 和 Promise.using().
(2)async / await
利用 async / await 中的 try...catch...finally 中的 finally .
6, 定时器
- (1)bluebird
- async function test() {
- try {
- let readFilePromise = new Promise((resolve, reject) => {resolve('result')})
- let result = await readFilePromise.delay(1000).timeout(2000, 'timed out')
- console.log(result);
- } catch (err) {
- console.log("error", err);
- }
- }
- test();
1, 默认的, new Promise 会立即执行, 但是加了 delay(), 可以延迟执行.
2,timeout() 可以设置执行的 timeout 时间, 超过即抛出 TimeoutError 错误.
(2)async / await
暂时没有方便的替代写法.
7, 实用方法
(1)bluebird
bluebird 的 Promise 中还包含了一些实用方法. tap 和 tapCatch 分别用来查看 Promise 中的结果和出现的错误. 这两个方法中的处理方法不会影响 Promise 的结果, 适合用来执行日志记录. call 用来调用 Promise 结果对象中的方法. get 用来获取 Promise 结果对象中的属性值. return 用来改变 Promise 的结果. throw 用来抛出错误. catchReturn 用来在捕获错误之后, 改变 Promise 的值. catchThrow 用来在捕获错误之后, 抛出新的错误.
(2)async / await
上面 bluebird 的实用方法, 在 async / await 的写法里, 显得无足轻重了.
8, 错误处理
(1)拓展 - then() 的多次指定与报错
对一个 resolve 的 promise , 指定多个 then:
- let promiseObj = new Promise((resolve, reject) => {resolve()})
- // 第一次指定 then
- promiseObj.then(function (data) {
- console.log("success1");
- }, function (data) {
- console.log("fail1");
- })
- // 第二次指定 then
- promiseObj.then(function (data) {
- console.log("success2");
- }, function (data) {
- console.log("fail2");
- })
- // 第三次指定 then
- promiseObj.then(function (data) {
- console.log("success3");
- })
- // 第四次指定 then(catch)
- promiseObj.catch(function (data) {
- console.log("fail4");
- })
输出:
- success1
- success2
- success3
对一个 reject 的 promise , 指定多个 then:
- let promiseObj = new Promise((resolve, reject) => {reject()})
- // 第一次指定 then
- promiseObj.then(function (data) {
- console.log("success1");
- }, function (data) {
- console.log("fail1");
- })
- // 第二次指定 then
- promiseObj.then(function (data) {
- console.log("success2");
- }, function (data) {
- console.log("fail2");
- })
- // 第三次指定 then
- promiseObj.then(function (data) {
- console.log("success3");
- })
- // 第四次指定 then(catch)
- promiseObj.catch(function (data) {
- console.log("fail4");
- })
输出:
- fail1
- fail2
- fail4
- Unhandled rejection undefined
结论:
1, 对于一个 promise 对象, 我们可以多次指定它的 then().
2, 当此 promise 状态变为 resolve, 即使没有 then() 或者 有 then() 但是没有 successCallback, 也不会有问题.
3, 当此 promise 状态变为 reject, 如果没有 then() 或者有 then() 但是没有 failureCallback , 则会报错(下面会介绍如何捕获这个错).
(2)bluebird
1, 本地错误处理
利用 then() 的 failureCallback(或 .catch() ). 不赘述了.
2, 全局错误处理
bluebird 提供了 promise 被拒绝相关的两个全局事件, 分别是 unhandledRejection 和 rejectionHandled :
- let promiseObj = new Promise((resolve, reject) => {reject('colin')})
- setTimeout(() => {
- promiseObj.catch(function (data) {
- console.log("fail");
- })
- }, 2000);
- process.on('unhandledRejection', (reason, promise) => console.error(`unhandledRejection ${reason}`));
- process.on('rejectionHandled', (reason, promise) => console.error(`rejectionHandled ${reason}`));
输出:
- unhandledRejection colin
- rejectionHandled [object Promise]
- fail
1,promise 的 reject 没有被处理(即上面所述), 则会触发 unhandledRejection 事件
2, 但可能 针对 reject 的处理延迟到了下一个事件循环才被执行, 那就会触发 rejectionHandled 事件
所以我们得多等等 rejectionHandled 事件, 防止误判, 所以可以写成下面全局错误处理的代码:
- let possiblyUnhandledRejections = new Map();
- // 当一个拒绝未被处理, 将其添加到 map
- process.on("unhandledRejection", function(reason, promise) {
- possiblyUnhandledRejections.set(promise, reason);
- });
- process.on("rejectionHandled", function(promise) {
- possiblyUnhandledRejections.delete(promise);
- });
- setInterval(function() {
- possiblyUnhandledRejections.forEach(function(reason, promise) {
- // 做点事来处理这些拒绝
- handleRejection(promise, reason);
- });
- possiblyUnhandledRejections.clear();
- }, 60000);
(3)async / await 的错误处理
async / await 的 try..catch 并不能完全捕获到所有的错误.
1, 本地错误处理
用 try...catch 即可.
注意: 漏掉错误 情况:
run() 这个 promise 本身 reject 了
- async function run() {
- try {
- // 注意这里没有 await
- return Promise.reject();
- } catch (error) {
- console.log("error",error)
- // 代码不会执行到这里
- }
- }
- run().catch((error) => {
- // 可以捕获
- console.log("error2", error)
- });
解决方法: 针对 run() 函数 (顶层函数)做好 catch 捕获.
2, 全局错误处理
漏掉错误 情况:
run() 这个 promise 内部存在 reject 但没有被处理的 promise
- async function run() {
- try {
- // 注意这里 即没有 await 也没有 return
- Promise.reject();
- } catch (error) {
- console.log("error", error)
- // 代码不会执行到这里
- }
- }
- run().catch((error) => {
- // 不可以捕获
- console.log("error2", error)
- });
解决方法:
1, 跟上面介绍的 bluebird 全局错误处理一样, 用好 unhandledRejection 和 rejectionHandled 全局事件.
2,ES6 原生也支持 unhandledRejection 和 rejectionHandled 全局事件.
参考资料
使用 bluebird 实现更强大的 Promise
来源: https://www.cnblogs.com/xjnotxj/p/12041074.html