Promiseでひとしきりドヤった後にasync/awaitを使った結果

前回の記事で少し触れたように、最近趣味で作っているアニメ視聴管理アプリの開発においてPromiseを多用していました。

Promise使わなくていいとこでも使っていたので、もはや非同期処理を捌いている自分に酔っていたと言っても過言では無いです。

しかし、さらに気分良くPromiseできるようにネットサーフィンして情報収拾していた際にasync/awaitに関する記事を読んでショックを受けたので、今回はasync/awaitについてです。

async/awaitとは

そもそも何なのかと言うと、Promiseをより簡潔に記述して・根本的に非同期処理のハンドリングができる関数です。
ES2017から追加された仕様で、ブラウザ対応状況としてはこんな感じです。

ショックだったといのはこのブラウザ対応状況を見てのことだったのですが、IEさえ切っていいのであれば普通に使って良さそうです。
もちろん実務で使うのであればBabel等通しますが、Promiseとそこまで対応状況に大差ないというのが衝撃でした。

よりスマートに非同期処理を捌けるのであれば使うべきでしょう。
明日から笑顔でドヤれるように早速試しに触っていきます。

async/awaitの背景

その前にasync/awaitが追加された背景についてですが、参考サイトによると以下の理由のようです。

  • そもそもPromiseはLinuxのsleepのように処理を停止させているわけではない(≒JavaScriptの非同期処理における課題を根本的に解決しているわけではない)
  • 実はPromiseの構文がけっこう複雑で直感的でなかった

つまり、Promiseをすでに使っている方にとっては更に恩恵を受けられる機能と言えますが、要は使いやすくしているものなのでPromiseに対する根本的な理解は依然として必要かなと思います(ブーメラン)。

基本的なパターン

同じ処理を従来のPromiseで書いた版とasync/await版で書いたもので比較していきましょう。
伝統に則ってsetTimeoutで非同期処理を再現するパターンです。

従来のPromise版

const asyncFunc = ()=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('success!!');
    },3000);
  });
};

asyncFunc().then((result) => {
  console.log(result);
});

async/await版

const asyncFunc = ()=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('success!!');
    },3000);
  });
};

(async () => { 
  const result = await asyncFunc();
  console.log(result);
})();

非同期処理を内包する関数においてPromiseをreturnする形式で定義するのは変わらないです。

呼び出し部分ですが、従来のPromise版ではresolveイベントと返り値をthenで受け取ることで非同期処理を制御していました。
async/await版の場合は、asyncを付与されている関数(今回の場合は即時関数)の中においては、awaitを付与された関数がresolveするまで処理を停止することで実現しています。

つまり大きな違いとしてはthenを使っていないということかと思います。

ただ、この例だとあまりasync/awaitの良さが伝わりづらいので次の例いってみます。

非同期処理を直列で実行するパターン

非同期処理Aが完了したら非同期処理B、非同期処理Bが完了したら非同期処理C、みたいなパターンですね。

従来のPromise版

const asyncFunc = (val)=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(val + 1);
    },3000);
  });
};


asyncFunc(0)
  .then((result) => {
    console.log(result);
    return asyncFunc(result);
  }).then((result) => {
    console.log(result);
    return asyncFunc(result);
  }).then((result) => {
    console.log(result);
    return asyncFunc(result);
  });

async/await版

const asyncFunc = (val)=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(val + 1);
    },3000);
  });
};

(async () => {
  let result = await asyncFunc(0);
  console.log(result);
  result = await asyncFunc(result);
  console.log(result);
  result = await asyncFunc(result);
  console.log(result);
})();

thenの記述が不要になったのと、awaitを付与さえた非同期処理の結果の値を変数に格納する形式にしたことで従来のPromise独自の構文がなくなってスッキリしました。

こうして置き換えた後のソースを見てみると『$.Deferredとかasync/awaitとか使わなくても本来はこういう風に動いて欲しかったんだよな…』と思ってしまいます。

しかし、そうなってくると逆に『いやいや、処理を一つずつ順番に実行してたら非効率でしょ』的な状況にもなるかと思いますが、従来のPromiseで実現していた並列処理ももちろん対応可能です。

非同期処理を並列で実行するパターン

従来のPromise版

const asyncFunc = (val)=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`done${val}`)
      resolve();
    },100*val );
  });
};

const promiseArray = [asyncFunc(30),asyncFunc(10),asyncFunc(1)];
Promise.all(promiseArray).then(() => {
  alert('All done');
});

async/await版

const asyncFunc = (val)=> {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`done${val}`)
      resolve();
    },100*val );
  });
};


(async () => {
  const async1 = asyncFunc(30);
  const async2 = asyncFunc(10);
  const async3 = asyncFunc(1);
  await async1;
  await async2;
  await async3;
  alert('All done');
})();

直列の場合と違って並列処理の場合は非同期処理の関数を実行する際にawaitを付与せずに変数に格納しています。
こうすることで、変数定義した時点で処理が実行されるので並列的になります。

そして、awaitで呼ばれている変数内のPromiseがresolveし終わった後にalertが呼ばれるという流れになっています。
逆に、例えばawait async1の記述をコメントアウトすると3秒待たずにalertが実行されます。

直列処理に関してはasync/awaitを使用することで良い感じになりましたが、並列処理の場合はPromise.all()を使った方が綺麗かな…?という微妙なサンプル実装になってしまいました。

使ってみての感想と気づき

  • これまでthenを使ってチェーンしていた直列処理はasync/await使うとメリットが大きい
  • 並列処理の場合、各非同期処理の結果をreduceみたいに一つの返り値にしたい場合以外はPromise.all()の方が可読性高そう
  • asyncはあくまで従来のPromiseを簡潔に扱いやすくしたものなので、ケースによってはthen()と組み合わせて使用してもよいのかもしれない
  • awaitはasync内でしか使えないので、asyncの外側(グローバル)の処理に対しても制御したいケースには向かない

おわり

実際に使ってみるとメリットだけではなくケースによってはデメリットや使わない方が良さそうな場合もありそうだと思いました。
ただ、それでもPromiseを使用する際にはぜひ選択肢としてasync/awaitでの実装を検討しておきたいです。

少なくとも自分はfirebaseから非同期とゴリゴリとデータを取得したり登録していた処理をasync/awaitを使ってスッキリさせてドヤろうかなと思ってます!

まだまだ理解が浅いのでもう少し使ってみて更に気づきがあれば記事にしたいと思います。

ではでは