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 的官网文档。

参考文献:

  • JavaScript Promise 迷你书(中文版)
  • ECMAScript 6 入门
  • 从地狱到天堂,Node 回调向 async/await 转变

你可能感兴趣的:(async/await 应用手札)