blog icon indicating copy to clipboard operation
blog copied to clipboard

使用async/await的异步并发请求

Open mrdulin opened this issue 7 years ago • 2 comments

问题:现有A,B,C三个接口,独立,相互之间没有依赖关系,因此需要并发,要求使用async/await语法并行发送3个请求,并有对应的异常处理。

首先模拟三个API接口请求(或者理解为异步操作)

interface IApiSuccessResponse {
  errorCode: number;
  content: any;
}

interface IError {
  type: string;
  err: Error;
}

interface IErrorMap {
  [type: string]: IError;
}

// true和false的几率各占50%,用来模拟接口出错的概率
const coin = (): boolean => Math.random() < 0.5;
//总的模拟接口错误开关,true是开启模拟接口的异常,开启后,将按照coin的概率给出错误
let errorSwitch: boolean = false;
//前端预定义的A,B,C接口错误信息,根据需要添加其他字段,例如errorCode
const ERROR_MAP: IErrorMap = {
  A: {
    type: 'A',
    err: new Error('A接口请求失败')
  },
  B: {
    type: 'B',
    err: new Error('B接口请求失败')
  },
  C: {
    type: 'C',
    err: new Error('C接口请求失败')
  }
};

/**
 * 模拟接口处理逻辑
 * @param type 根据type模拟接口请求返回不同的结果或异常,为了演示设置的变量,减少重复代码。
 * @param resolve 
 * @param reject 
 */
const handle = (type, resolve, reject) => {
  setTimeout(() => {
    console.log(type, Date.now());
    const error = ERROR_MAP[type];
    const apiRes: IApiSuccessResponse & { type: string } = { type: type.toLowerCase(), errorCode: 0, content: `${type} SUCCESS` };
    if (errorSwitch) {
      coin() ? resolve(apiRes) : reject(error);
    } else {
      resolve(`${type} SUCCESS`);
    }
  }, 3000);
};

/** 
 * 模拟A,B,C三个接口请求(或者理解为异步操作)
 */
const A = () => new Promise((resolve, reject) => handle.call(null, 'A', resolve, reject));
const B = () => new Promise((resolve, reject) => handle.call(null, 'B', resolve, reject));
const C = () => new Promise((resolve, reject) => handle.call(null, 'C', resolve, reject));

紧接着,一个错误的思路:以为A,B,C三个请求按照代码书写顺序从上到下书写,就是并行执行,以为和普通请求的并行的书写方式一样。

//下面这个例子是不使用`async/await`,`A,B,C`三个请求并发的例子。
function m() {
  A();
  B();
  C();
}
m();
/*输出结果:
A 1510143604973
B 1510143604976
C 1510143604977
*/

//使用async/await,下面写法实际上三个请求是串行执行
async function main() {
  errorSwitch = false;
  let error;
  try {
    const resultA = await A();
    const resultB = await B();
    const resultC = await C();
  } catch (e) {
    //任何一个请求出错,代码执行流程会立即进入catch,意味着后面的请求不会发送,这可能不是我们想要的结果,因为A,B,C三个请求是独立的,没有依赖关系的。
    //A请求的失败,不应该影响B,C请求。
    error = e;
  }
  console.error(error);
}
main();
/*输出结果:
A 1510143604977
B 1510143607981
C 1510143610982
*/

一个可行的思路:

interface IStateModel {
  errorMessage?: string;
  data?: any;
}

interface IState {
  [type: string]: IStateModel;
}

let state: IState = {
  //a, b, c三个API请求结果,对应视图上三块区域,如果API请求成功,将请求结果赋值给相应的变量的data字段,如果请求失败,则将错误信息赋值给errorMessage变量
  a: {
    errorMessage: '',
    data: {}
  },
  b: {
    errorMessage: '',
    data: {}
  },
  c: {
    errorMessage: '',
    data: {}
  }
};
function setState(callback: (state: IState) => IState) {
  const nextState: IState = callback.call(null, state);
  state = nextState;
  const tpl: string = render();
  console.log(tpl);
  /** 
   *  <section>
   *     <p>A SUCCESS</p>
   *     <p>B接口请求失败<p/>
   *     <p>C SUCCESS</p>
   *   </section>
   */
}
function render() {
  console.log('nextState: ', state);
  const { a, b, c } = state;
  return `
    <section>
      ${a.errorMessage ? `<p>${a.errorMessage}<p/>` : `<p>${a.data}</p>`}
      ${b.errorMessage ? `<p>${b.errorMessage}<p/>` : `<p>${b.data}</p>`}
      ${c.errorMessage ? `<p>${c.errorMessage}<p/>` : `<p>${c.data}</p>`}
    </section>
  `;
}

async function componentDidMount() {
  errorSwitch = true;
  let results: IState[];
  try {
    //Promise.all的行为是:A,B,C都resolve,则resolve, 只要有一个reject, 则reject,这明显不是我们需要的。
    //我们需要的是,不论A,B,C哪个reject,Promise.all依旧resolve,
    //所以需要将[A(), B(), C()]这个可能包含rejected promise的promise数组通过map方法映射一组新的promise数组,这个新的promise数组中promise都是resolve的
    results = await Promise.all([A(), B(), C()].map((promise) => {
      return promise
        //接口正常,将请求结果包装成特定的数据结构返回
        .then((apiRes: IApiSuccessResponse & { type: string }): IState => {
          return {
            [apiRes.type]: {
              data: apiRes.content
            }
          };
        })
        //接口异常,包装前端预定义好的错误信息返回
        .catch((e: IError) => {
          const { type, err } = e;
          let error: IState = {
            [type.toLowerCase()]: {
              errorMessage: err.message
            }
          };
          return error;
        });
    }));

    setState((prevState: IState): IState => {
      let nextState: IState = {};
      results.forEach((result: IState, idx: number) => {
        const type = Object.keys(result)[0];
        const data: IStateModel = result[type];
        nextState[type] = {};
        Object.assign(nextState[type], prevState[type], data);
      });
      return nextState;
    });
  } catch (e) {
    //捕获其他异常
    console.error('componentDidMount', e.message);
  }

}
componentDidMount();

export { };

总结:对于串行请求,async/await相对于Promise的写法,比较有优势,不用链式then,但依旧是个人编码习惯的问题。有人觉得使用try...catch配合async/await顺手,也有人觉得链式then更顺手。

对于并行请求,以上面的例子为例,使用Promise.all包了一层,和Promise的写法并没有太大不同。但依旧需要测试比对各种场景,比如在for循环中使用async/await

对于async/await串行请求的方式,需要注意的是: await语句后面的程序将不会执行,直到await语句执行完毕(可以理解为异步操作完成),所以,一些需要并行执行的操作,例如发请求的同时进行DOM操作,动画,赋值,计算等,注意不要写在await语句后面。

mrdulin avatar Nov 09 '17 07:11 mrdulin

@18566246732

Promise.all的行为是:A,B,C都resolve,则resolve, 只要有一个reject, 则reject,这明显不是我们需要的。 我们需要的是,不论A,B,C哪个reject,Promise.all依旧resolve, 所以需要将[A(), B(), C()]这个可能包含rejected promise的promise数组通过map方法映射一组新的promise数组,这个新的promise数组中promise都是resolve的

mrdulin avatar Jan 30 '18 13:01 mrdulin

const P1 = A(); const P2 = B(); const P3 - C();

const resultA = await P1; const resultB = await P2(); const resultC = await P3();

wulisensen avatar Aug 21 '20 03:08 wulisensen