async/await 应用手札

[注:以下代码都在支持 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对象全部变为resolvereject状态的时候,它才会去调用.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.3 Promise.race

Promise.racePromise.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 PromisePromise.resove的另一个作用是将thenable对象转换为promise对象。
ES6 Promise里提到了Thenable的概念,简单来讲它是非常类似于promise的东西。就好像有些具有.length方法的非数组对象被称为Array likethenable指的是具有.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)。但两者有不同之处:

  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 对象。如果不是,会被转化为一个立即 resolvePromise 对象。
只要一个 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 的官网文档。

参考文献:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容