[注:以下代码都在支持 Promise 的 Node 环境中实现]
1 promise 释义
promise 是抽象异步处理的对象,其提供了一系列处理异步操作的方法。
1.1 语法
const promiseA = new Promise((resolve, reject)=>{
// 异步操作
// 操作结束,使用 resolve()返回结果;使用 reject()处理错误
})
promiseA.then(onFulfilled, onRejected);
例子1-1:
const promiseA = new Promise(()=>{
setTimeout(()=>{
resolve('3秒后返回了A');
}, 3000)
});
promiseA.then((res)=>{
console.log(res);
});
1.2 static method
像 Promise
这样的全局对象还拥有一些静态方法。
包括 Promise.all()
还有 Promise.resolve()
等在内,主要都是一些对Promise进行操作的辅助方法。
1.2.1 Promise.all
Promise.all
接收一个promise
对象数组作为参数,当这个数组里的所有promise
对象全部变为resolve
或reject
状态的时候,它才会去调用.then
方法。
用于需要同时触发多个异步操作,并在所有异步操作都执行结束以后才调用.then
。
Promise.all
里有一个 promise
返回错误的时候就调用 catch()
了。测试代码如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise A.');
}, 1000);
});
const promiseB = new Promise((resolve, reject) => {
setTimeout(() => {
reject('error B');
// resolve('promise B');
}, 1500);
});
const promiseC = new Promise((resolve, reject) => {
setTimeout(() => {
reject('error c');
}, 1000);
});
Promise.all([promiseA, promiseB, promiseC]).then((res)=>{
console.log(res);
}).catch((err) => {
console.log(err);
});
// 结果:error c
这点和预期的不同。具体描述可以看 MDN 的文档,这里摘录一部分:
The
Promise.all()
method returns a singlePromise
that resolves when all of the promises in the iterable argument have resolved or when the iterable argument contains no promises. It rejects with the reason of the first promise that rejects.
- 思考:那么在并行执行所有
promise
过程中,在存在reject
的情况下如何获取其余resolve
的全部结果?
似乎并没有单独的method
来处理,需要封装一个方法。
1.2.3 Promise.race
Promise.race
和Promise.all
类似,同样对多个promise
对象进行处理,同样接收一个promise对象数组。Promise.race
只要有一个promise
对象进入Fullfilled
或者Rejected
状态的话,就会执行.then
或.catch
方法。
测试代码如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise A.');
}, 1000);
});
const promiseB = new Promise((resolve, reject) => {
setTimeout(() => {
// reject('error B');
resolve('promise B');
}, 1500);
});
const promiseC = new Promise((resolve, reject) => {
setTimeout(() => {
// reject('error c');
resolve('promise c');
}, 500);
});
Promise.race([promiseA, promiseB, promiseC]).then((res)=>{
console.log(res);
}).catch((err) => {
console.log(err);
});
1.2.2 Promise.resolve
静态方法Promise.resolve(value)
可以认为是new Promise()
方法的快捷方式。如:
Promise.resolve(42).then((value)=>{
console.log(value);
})
但初始化Promise
对象建议仍然使用new Promise
,Promise.resove
的另一个作用是将thenable
对象转换为promise
对象。
ES6 Promise里提到了Thenable
的概念,简单来讲它是非常类似于promise
的东西。就好像有些具有.length
方法的非数组对象被称为Array like
,thenable
指的是具有.then
方法的对象。
这种将thenable
对象转换为promise
对象的机制要求thenable
对象所拥有的then
方法应该和Promise
所拥有的then
方法具有同样的功能和处理过程,在将thenable
对象转换为promise``对象的时候,还会巧妙的利用thenable
对象原来具有的then
方法。最简单的例子就是jQuery.ajax()
,它的返回值就是thenable
。下面看看如何将thenable
对象转换为promise
对象。
const promiseA = Promise.resolve($.ajax('/json/comment.json')); // => promise 对象
promiseA.then((value)=>{
console.log(value);
})
需要注意的是jQuery.ajax()
返回的是一个具有.then
方法的jqXHR Object
对象,这个对象继承了来自Deferred Object
的方法和属性。
但是Deferred Object
并没有遵循PormisesA+
或ES6 Promises
标准,所以即使看上去对象转换为了promise
对象,其实还是缺失了部份信息。即使一个对象具有.then
方法,也不一定就能作为ES6 Promises
对象使用。
这种转换 thenable
的功能除了在编写使用Promises
的类库的时候需要了解之外,通常作为end-user
不会使用到此功能。
1.2.3 Promise.reject
通过调用Promise.reject()
可以将错误对象传递给onRejected
函数。
Promise.reject(new Error("BOOM!"))
.catch((error)){
console.log(error);
}
这个方法并不常用。
1.3 promise 状态
用 new Promise
实例化的 promise 对象有三种状态:
- 'has resolution' => 'Fulfilled'
resolve(成功)时,会调用 onFulfilled。 - 'has rejected' => 'Rejected'
reject(失败)时,会调用 onRejected。 - 'unresolved' => 'Pending'
promise 对象刚被创建后的初始状态。
promise对象的状态,从 Pending 转换为 Fulfilled 或 Rejected 之后,promise 对象的状态就不再改变。因此,在 .then()
内执行的函数只会调用一次。
异常处理:then or catch?
.catch
方法可以理解为 promise.then(undefined, onRejected)
。但两者有不同之处:
- 使用promise.then(onFulfilled, onRejected) 的话,在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
- 在 promise.then(onFulfilled).catch(onRejected) 的情况下,then 中产生的异常能在 .catch 中捕获。
- then 和 .catch 在本质上是没有区别的,但需要根据1,2点的差异选择适用的场合。
测试对比代码如下:
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1s test.');
}, 1000);
});
promiseA.then((res)=>{
throw new Error('handler err');
}).catch((err)=>{
console.log(`promiseA ${err}`);
})
const promiseA = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1s test.');
}, 1000);
});
promiseA.then((res) => {
throw new Error('handler err');
}, (err)=>{
console.log(`promiseA ${err}`);
});
2 async/await 简介
Node7 通过 --harmony_async_await
参数支持 async/await ,而 async/await 由于其可以用同步形式的代码书写异步操作,能彻底杜绝‘回调地狱’式代码。
async/await 基于 Promise
, 是 Generator
函数的语法糖。async
函数返回一个 Promise
对象,可以使用 then
方法添加回调函数。当函数执行时,一旦遇到await
就先返回,等到触发的异步操作完成,再接着执行函数体后面的语句。示例代码如下:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('测试 async/await');
}, timer);
});
}
async function test() {
const time0 = new Date();
const res = await asynchornous(2000);
const time1 = new Date();
console.log(`返回 => ${res},用时:${Math.floor((time1 - time0)/1000)}s`);
}
test();
2.1 await 的用法
await
命令必须用到 async
函数中,且其后应该是一个 Promise
对象。如果不是,会被转化为一个立即 resolve
的 Promise
对象。
只要一个 await
命令后面的 Promise
对象变为 reject
状态,那么整个 async
函数都会中断执行。
async function test() {
await Promise.reject('error');
await Promise.resolve('test'); // 不会执行
}
这时如果我们希望前一个异步操作失败后,不中断后面的异步操作,可以捕获前一个异步操作的错误。另一种写法是在 await
后面的 Promise
对象后再跟上 catch
方法。示例代码如下:
async function test() {
await Promise.reject('error')
.catch(err => console.log(err));
const res = await Promise.resolve(`test`);
console.log(res);
}
test();
// 执行结果:
// error
// test
2.2 捕获错误
await
命令后的 Promise
对象,运行结果可能是 rejected,这样等同于 async
函数返回的 Promise
状态为 rejected
。 所以可以把 await 命令放到 try...catch 代码中。示例代码如下:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// resolve('测试 async/await');
reject('error test');
}, timer);
});
}
async function test() {
const time0 = new Date();
let res = '...';
try {
res = await asynchornous(2000);
} catch (error) {
console.log(`返回 => ${error}`);
}
const time1 = new Date();
console.log(`返回 => ${res},用时:${Math.floor((time1 - time0)/1000)}s`);
}
test();
// 执行后返回结果如下:
// 返回 => error test
// 返回 => ...,用时:2s
2.3 并发执行
如果 多个 await
后面的异步操作,不存在依赖关系,那么最好让它们都并发执行。使用 Promise.all
可以让多个 promise
并发,同时还有另一种写法。
示例代码如下:
// 写法一
let [resA, resB] = await Promise.all([testA(), testB]);
// 写法二
let proA = testA();
let proB = testB();
let resA = await proA;
let resB = await proB;
上述写法,testA 和 testB 都是同时触发的。那么再看看继发执行的代码:
let resA = await proA();
let resB = await proB();
3 改写 callback 方式
Node 很多库函数,还有很多第三方库函数都是使用回调实现,那么要如何修改为 Promise 实现?
- 使用第三方库,如:Async,Q,Bluebird 等,具体实现请参考官方文档和附录参考3。
- 自己实现一个将回调风格转变为 Promise 风格的类库。
这里详细讲解如何实现回调函数的转换函数。
3.1 定义 promisify()
promisify
是一个转换函数,它的参数是需要转换的回调函数,那么返回值则是一个返回 promise
对象的函数。如下:
function promisify(callback) {
return function(){
return new Promise((resolve, reject)=>{
// TODO:
})
}
}
3.2 Promise 中调用 callback
要让回调函数在 Promise 中调用,并且根据结果适当的调用resolve()
和reject()
。
function promisify(callback) {
return function(){
return new Promise((resolve, reject)=>{
callbacn((error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
})
})
}
}
注意,Node 回调函数第一个参数都是错误对象,如果为 null 表示没有错误。
3.3 添加参数
继续添加处理参数的代码。Node 回调函数通常前面 n 个参数是内部实现需要使用的参数,而最后一个参数是回调函数。因此可以使用 ES6 的可变参数和扩展数据语法来实现。代码如下:
function promisify(callback) {
return function(...args){
return new Promise((resolve, reject)=>{
callback(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
})
})
}
}
3.4 实现 promisifyObject()
顾名思义,promisifyObject()
是用来转换对象中异步方法的回调函数。转换函数必须考虑this
指针的问题,所以不能直接使用上面的一般实现。下面是 promisify()
的简化实现,详情请参考代码中的注释。
function promisifyObject(obj, suffx = 'Promisified') {
// 参照之前的实现,重新实现 promisify.
// 这个函数没用到外层的局部变量,不必实现局域函数
// 这里实现为局部函数只是为了组织演示代码
function promisify(callback){
return function(...args) {
return new Promise((resolve, reject) => {
// 注意调用的方式有了改变
callback.call(this, ...args, (error, result) => {
if (error) {
reject(error)
} else {
resolve(result)
}
})
})
}
}
// 先找出所有方法名称
// 如果需要过滤可以添加 filter 实现
const keys = [];
for (const key in obj) {
if(typeof obj[key] === 'function') {
keys.push(key);
}
}
// 将转换之后的函数仍然附加到原对象上,
// 以确保调用时候,this 引用正确。。
// 为了避免覆盖原函数,`promise`风格的函数名前添加‘suffix’.
keys.forEach(key => {
obj[`${key}${suffix}`] = promisify(obj[key]);
})
return obj;
}
3.5 将转换 Promise
的函数封装成模块
实现很简单,具体代码如下:
module.exports = {
promisify,
promisifyObjecj
}
// 通过解构对象导入
// const {promisify, promisifyObject} = require('./promisify');
3.6 实际场景应用
这里使用实际项目中用到的 qiniu api 存图场景中异步回调被改写后如何使用 async/await
,示例代码如下:
function saveImage(...args) {
// bucketManager 是 qiniu api 里操作存储空间的对象,
// .fetch 方法是用来上传内容的方法
return new Promise((resolve, reject) => {
bucketManager.fetch(resUrl, bucket, key, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
async function expand() {
try {
const response = await saveImage('', 'hexo', 'qiuniu_api_test.jpg');
console.log('res', response);
} catch (error) {
console.log('err', error);
}
}
expand();
4 jest 测试
最后我们尝试使用 jest
来测试以 Promise
为基础的异步代码。
示例1:
function sleep(timer, state) {
return new Promise(((reslove) => {
setTimeout(() => {
// things
reslove('sleep:ok');
if (state === 404) {
throw new Error('sleep:这里有个 404');
}
}, timer);
}));
}
// The assertion for a promise must be returned.
it('works with promises', () => {
expect.assertions(1); // ?
return sleep(1000, 200).then(result => expect(result).toEqual('sleep:ok'));
});
示例1 测试的返回 promise
示例的函数,需要设置 expect.assertions(1)
,然后将期望函数写到 .then
方法中即可。
示例2:
function asynchornous(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('测试 async/await');
}, timer);
});
}
// async/await can be used.
it('works with async/await', async () => {
expect.assertions(1);
const data = await asynchornous(1000);
expect(data).toEqual('测试 async/await');
});
代码同样很简单,更多的示例可以查看 jest
的官网文档。
参考文献:
- JavaScript Promise 迷你书(中文版)
- ECMAScript 6 入门
- 从地狱到天堂,Node 回调向 async/await 转变