async/await 应用探索

综合技术 2018-03-14

tags: node,async,await,promise

[注:以下代码都在支持 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 single Promise
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.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.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.4 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)
。但两者有不同之处:

  1. 使用promise.then(onFulfilled, onRejected) 的话,在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
  2. 在 promise.then(onFulfilled).catch(onRejected) 的情况下,then 中产生的异常能在 .catch 中捕获。
  3. 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 代码中。示例代码如下:

```js

tags: node,async,await,promise

[注:以下代码都在支持 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 single Promise
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.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.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.4 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)
。但两者有不同之处:

  1. 使用promise.then(onFulfilled, onRejected) 的话,在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
  2. 在 promise.then(onFulfilled).catch(onRejected) 的情况下,then 中产生的异常能在 .catch 中捕获。
  3. 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 实现?

  1. 使用第三方库,如:Async,Q,Bluebird 等,具体实现请参考官方文档和附录参考3。
  2. 自己实现一个将回调风格转变为 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
的官网文档。

参考文献:

tags: node,async,await,promise

[注:以下代码都在支持 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 single Promise
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.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.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.4 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)
。但两者有不同之处:

  1. 使用promise.then(onFulfilled, onRejected) 的话,在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。
  2. 在 promise.then(onFulfilled).catch(onRejected) 的情况下,then 中产生的异常能在 .catch 中捕获。
  3. 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 实现?

  1. 使用第三方库,如:Async,Q,Bluebird 等,具体实现请参考官方文档和附录参考3。
  2. 自己实现一个将回调风格转变为 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
的官网文档。

参考文献:

您可能感兴趣的

Can I set the local time zone in my browser via Ja... I know I can get the local timezone offset via new Date().getTimeZoneOffset(). But where did Javascript get that information? Is there a way I can set...
Is there a way to remove the static statement from... For a site I'm developing I have two html buttons, not ASP because I do not want them to postback. For the submit button I am calling a javascript fun...
在别人春节休息的时候,做一个项目,只为来年找工作(讲义)... 这标题很学霸吧。 以下是饥人谷「前端系统班」的第 47 节课《网易云音乐第一课》的讲义。 写代码啦! 目标 手写一个网易云音乐手机站点: 网易云音乐 需求分析 架构图 搭建 LeanCloud 环境 ...
Is it possible to define a method of privilege out... Well I have this constructor with one secret and one method: function Keeper(get) { var secretPower = 'wisdom';this.get = get ? get : func...
ECharts 3.6.1 发布,JavaScript 图表库 ECharts 3.6.1 发布了,本次更新如下: 修正了 data sampling 的失效。 修正了 使用 webpack 编译时在 ie11 中的问题。参见 zrender/#189 。 下载地址 和 更新内容 ECharts 是...
0
小专栏

责编内容来自:小专栏 (本文源链)
阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。