新型コロナウィルスの感染拡大防止にあたりご確認ください

新型コロナウィルスの感染拡大防止の為、一定期間はお電話でのカウンセリングをご案内させていただきます。

みなさまの安全に配慮し、発熱や風邪の症状がある、海外渡航歴がある場合にも、電話カウンセリングをお願いしております。それ以外の方でも、交通機関での移動にご不安を感じられる方は同様に電話カウンセリングも可能でございます。一人一人に合った支援をさせていただきますので、ご希望がございましたら登録後カウンセリングに進む際にお申し付けください。

【翻訳記事】ASYNC/AWAITを使ってコードをシンプルにする方法

Patrick Triest氏がブログで投稿した記事「ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER (Posted Aug 11, 2017)」を翻訳してご紹介しています。
なお、この記事は原著者の許諾を得て翻訳・掲載しています。


コールバック関数を書くのを止め、JAVASCRIPT ES8を愛した理由

時には、現代のJavascriptプロジェクトが手に負えなくなることもあります。この主な原因は、非同期タスクの面倒な処理です。この処理によって、コードブロックは、長く複雑で深く入れ子になってしまいます。Javascriptは、これらの操作を処理するための新しい構文を提供するようになりました。それによって、最も複雑な非同期操作を簡潔で読みやすいコードに変えることができます。

背景

AJAX(非同期JavaScriptとXML)

最初に簡単な歴史を説明します。1990年代後半、非同期Javascriptの最初の大きな打開策としてAjaxが生まれました。この技術により、HTMLが読み込まれた後にウェブサイトが新しいデータを引き出して表示できるようになりました。これは、ほとんどのウェブサイトがコンテンツの更新を表示するためにページ全体を再度ダウンロードしなければならない時代において、革新的なアイデアでした。この技術(jQueryのバンドルされたhelper関数によって名前が普及した)は、2000年代のすべてのウェブ開発を支配していました。そして、Ajaxは、現在ウェブサイトがデータの検索に使用する主な技術ですが、JSONの代わりにXMLが使われています。

NodeJS

NodeJSが2009年に最初に発表されたとき、サーバーサイド環境の主な焦点は、プログラムが適切に同時(並行)実行を処理できるようにすることでした。当時のほとんどのサーバーサイド言語は、操作が終了するまでコード補完をブロックすることによってI/O操作(入出力操作)を処理しました。Nodejsは代わりに、イベントループアーキテクチャを利用しました。これにより、開発者は、Ajax構文が機能するのと同様の方法で、ノンブロック非同期操作が完了した後にトリガーされる「コールバック」関数を割り当てることができます。

Promise

数年後、NodeJSとブラウザ環境の両方で「Promise」という新しい標準が登場しました。Promiseは、非同期操作を合成するための強力かつ標準化された方法を提供します。Promiseは依然としてコールバックベースのフォーマットを使用していましたが、非同期操作のチェイニングと合成のための一貫した構文を提供していました。人気のあるオープンソースライブラリによって開拓されたPromiseは、最終的に2015年にJavascriptのネイティブ機能として追加されました。

Promiseは大きく改善された標準でしたが、やや冗長で読みにくいコードブロックの原因となることがよくありました。

ようやくその解決策が登場しました。

async/awaitは、コールバックを持たない通常の同期関数であるかのようにPromiseを合成する新しい構文です(.NETとC#から借りています)。async/awaitは、昨年のJavascript ES7で追加された、Javascript言語における素晴らしい構文であり、既存のJSアプリケーションを単純化するために使用することができます。

使用例

いくつかのコードの例を紹介します。

これらの例を実行するために必要なライブラリはありません。async/awaitは、Chrome、Firefox、Safari、Edgeの最新バージョンで完全にサポートされているため、ブラウザコンソールで例を試すことができます。さらに、async/await構文は、Nodejsバージョン7.6以降で動作し、BabelとTypescriptのトランスバイラによってサポートされているため、現在のすべてのJavascriptプロジェクトで実際に使用することができます。


セットアップ

自分のマシンを使って理解したい場合は、このダミーのAPIクラスを使用してください。このクラスは、呼び出された後の200msの単純なデータで解決されるPromiseを返すことによって、ネットワーク呼び出しをシミュレートします。

class Api {
  constructor () {
    this.user = { id: 1, name: 'test' }
    this.friends = [ this.user, this.user, this.user ]
    this.photo = 'not a real photo'
  }

  getUser () {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.user), 200)
    })
  }

  getFriends (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.friends.slice()), 200)
    })
  }

  getPhoto (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.photo), 200)
    })
  }

  throwError () {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Intentional Error')), 200)
    })
  }
}

それぞれの例では、①ユーザーを取得する、②友人を取得する、③その画像を取得するという3つの同じ操作を順番に実行します。最後に、3つの結果すべてをコンソールに記録します。

試行1 - 入れ子になった(ネスト化した)Promiseコールバック関数

入れ子になった(ネスト化した)Promiseコールバック関数を使用した実装は以下のとおりです。

function callbackHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.getPhoto(user.id).then(function (photo) {
        console.log('callbackHell', { user, friends, photo })
      })
    })
  })
}

これは、おそらくJavascriptプロジェクトに関わったことがある方はよくご存知でしょう。合理的に単純な目的を持っているコードブロックは長く深い入れ子になっていて、このように終わります……。

      })
    })
  })
}

実際のコードベースでは、それぞれのコールバック関数がかなり長くなる可能性があり、長く大きくインデントされた関数になる可能性があります。コールバック内のコールバック内のコールバックを処理するこのタイプのコードを処理することは、一般的に「コールバック地獄」と呼ばれています。

さらに悪いことに、エラーチェックが行われないので、コールバックのいずれかがPromiseのunhandled rejection(未処理の拒否)として何の表示もないまま失敗する可能性があります。

試行2 - Promiseの連鎖

改善できるかどうか見てみましょう。

function promiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('promiseChain', { user, friends, photo })
    })
}

Promiseの素晴らしい特徴の一つは、それぞれのコールバックの中で別のPromiseを返すことによって連鎖させることができることです。そのため、すべてのコールバックを同じインデントレベルに保つことができます。また、コールバック関数の宣言を省略するためにarrow関数も使用しています。

このバリアントは、前のものよりも読みやすく、連続性が優れていますが、まだ非常に冗長で、少し複雑です。

試行3 - async/await

コールバック関数を使用せずに書くことができるとしたらどうでしょう?不可能だと思いますか?7行で書いてしまうというのはどうでしょう?

async function asyncAwaitIsYourNewBestFriend () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}

ずっといいですね。Promiseの前に「await」を呼び出すと、Promiseが解決されるまで関数のフローが一時停止され、その結果が等号の左側の変数に代入されます。このようにして、あたかもそれが通常のコマンドの同期列であるかのように非同期操作フローをプログラムすることができます。

この時点で皆さんが私と同じくらい興奮していることを願っています。

「async」は関数宣言の最初で宣言されていることに留意してください。これは必須事項であり、関数全体をPromiseに変えます。このことについては後に掘り下げます。

ループ

async/awaitを使用すると、これまでの複雑な操作を簡単に行うことができるようになります。例えば、ユーザーの友人のそれぞれの友人リストを順番に取得したい場合はどうすればよいのでしょう?

試行1 - 再帰的Promiseループ

以下は、通常のPromiseを使って各友人のリストを順番に取り出すコードです。

function promiseLoops () {  
  const api = new Api()
  api.getUser()
    .then((user) => {
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      const getFriendsOfFriends = (friends) => {
        if (friends.length > 0) {
          let friend = friends.pop()
          return api.getFriends(friend.id)
            .then((moreFriends) => {
              console.log('promiseLoops', moreFriends)
              return getFriendsOfFriends(friends)
            })
        }
      }
      return getFriendsOfFriends(returnedFriends)
    })
}

リストが空になるまで友人の友人を取り出すために、Promiseを再帰的に連鎖させる内部関数を作り出しています。げげっ。これは完全に関数的で、それ自体はいいのですが、かなり簡単な作業のために非常に複雑な解決策を施しているように思えます。

注 - Promise.all()を使用してpromiseLoops()関数を単純化しようとすると、かなり異なる方法で振る舞う関数になってしまいます。この例の目的は、操作を順番に(一度に1つずつ)実行することですが、Promise.all()は非同期操作を同時に(すべてを一度に)実行するために使用されます。しかし、次のセクションで説明しますが、Promise.all()はasync/awaitと組み合わせても非常に強力です。

試行2 - async/awaitのFor-Loop

こちらの方がはるかに簡単かもしれません。

async function asyncAwaitLoops () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)

  for (let friend of friends) {
    let moreFriends = await api.getFriends(friend.id)
    console.log('asyncAwaitLoops', moreFriends)
  }
}

再帰的なPromiseの閉じを書く必要はありません。for-loopを書くだけで大丈夫です。async/awaitがあなたのために動いてくれます。

並行処理

それぞれの追加の友人リストを1つずつ取得するのは少し遅いですね。並行して取得するのはどうでしょう?async/awaitを使って実行することはできるのでしょうか?

もちろん、可能です。それによってすべての問題を解決してくれます。

async function asyncAwaitLoopsParallel () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const friendPromise = friends.map(friend => api.getFriends(friend.id))
  const moreFriends = await Promise.all(friendPromise)
  console.log('asyncAwaitLoopsParallel', moreFriends)
}

並行して操作を実行するには、実行するPromiseの配列を形成し、パラメータとしてPromise.all()に渡します。これは、awaitに単一のPromiseを返し、すべての操作が完了したら解決します。

エラー処理

しかし、非同期プログラミングにおいて、まだ取り組んでいない重要な問題が一つあります。それはエラー処理です。多くのコードベースの災いである非同期エラー処理では、各操作の個々のエラー処理コールバックを記述することがよくあります。コールスタックの先頭にエラーをパーコレートすると複雑になる可能性があり、通常、すべてのコールバックの開始時にエラーがスローされたかどうかをはっきりと確認する必要があります。このアプローチは退屈で、冗長で、エラーを起こしやすいです。さらに、Promiseにスローされた例外は、適切に捕らえなければ何の表示もないまま失敗するため、不完全なエラーチェックを伴うコードベースでは「目に見えないエラー」につながってしまいます。

例に戻って、それぞれにエラー処理を追加してみましょう。エラー処理をテストするために、ユーザーの写真を取得する前に、追加の関数である「api.throwError()」を呼び出します。

試行1 - Promiseエラーコールバック

最悪のシナリオを見てみましょう。

function callbackErrorHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.throwError().then(function () {
        console.log('Error was not thrown')
        api.getPhoto(user.id).then(function (photo) {
          console.log('callbackErrorHell', { user, friends, photo })
        }, function (err) {
          console.error(err)
        })
      }, function (err) {
        console.error(err)
      })
    }, function (err) {
      console.error(err)
    })
  }, function (err) {
    console.error(err)
  })
}

これは、単純に最悪です。非常に長くて不格好であるだけでなく、制御フローが、通常の読み取り可能なコードのように上から下ではなく、外側から内側にフローしているため、全くもって直観的ではありません。最悪です。次に進みましょう。

試行2 - Promise連鎖の「Catch」メソッド

Promiseの「Catch」メソッドを組み合わせることで、この状態を少し改善することができます。

function callbackErrorPromiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.throwError()
    })
    .then(() => {
      console.log('Error was not thrown')
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('callbackErrorPromiseChain', { user, friends, photo })
    })
    .catch((err) => {
      console.error(err)
    })
}

こちらの方がいいですね。Promise連鎖の最後にある単一のCatch関数を利用することで、すべての操作に対して単一のエラー処理を行うことができます。しかし、まだ複雑です。そのため、通常のJavascriptエラーと同じ方法でそれらを処理するのではなく、特別なコールバックを使用して非同期エラーを処理する必要があります。

試行3 - 通常のTry/ Catchブロック

さらに改善できます。

async function aysncAwaitTryCatch () {
  try {
    const api = new Api()
    const user = await api.getUser()
    const friends = await api.getFriends(user.id)

    await api.throwError()
    console.log('Error was not thrown')

    const photo = await api.getPhoto(user.id)
    console.log('async/await', { user, friends, photo })
  } catch (err) {
    console.error(err)
  }
}

ここでは、通常のTry/Catchブロックで操作全体を包み込んでいます。このようにすると、同期コードと非同期コードから全く同じ方法でエラーをスローして捕らえることができます。ずっとシンプルです。

コンポジション

先ほど、「async」とタグ付けされた関数は実際にPromiseを返すと述べました。これによって、非同期制御フローを簡単に合成することができます。

例えば、先ほどの例を再構成して、ログに記録する代わりにユーザーデータを返すことができます。次に、Async関数をPromiseとして呼び出すことによって、データを取得することができます。

async function getUserInfo () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  return { user, friends, photo }
}

function promiseUserInfo () {
  getUserInfo().then(({ user, friends, photo }) => {
    console.log('promiseUserInfo', { user, friends, photo })
  })
}

さらに良い点として、レシーバ関数でもasync/await構文を使用することができます。これにより、非同期プログラミングが非常にはっきりとした、取るに足らないブロックになります。

async function awaitUserInfo () {
  const { user, friends, photo } = await getUserInfo()
  console.log('awaitUserInfo', { user, friends, photo })
}

では、最初の10名のユーザーのすべてのデータを取得しなければならない場合はどうでしょう?

async function getLotsOfUserData () {
  const users = []
  while (users.length < 10) {
    users.push(await getUserInfo())
  }
  console.log('getLotsOfUserData', users)
}

並行処理は行っていますか?また、隙のないエラー処理関数を備えていますか?

async function getLotsOfUserDataFaster () {
  try {
    const userPromise = Array(10).fill(getUserInfo())
    const users = await Promise.all(userPromise)
    console.log('getLotsOfUserDataFaster', users)
  } catch (err) {
    console.error(err)
  }
}

結論

シングルページのjavascript ウェブアプリケーションの登場とNodeJSの採用の拡大に伴い、Javascript開発者にとって、並行処理を適切に処理することがこれまで以上に重要になっています。async/awaitは、何十年もJavascriptコードベースを悩ましていたバグを誘発する制御フローの問題の多くを軽減します。また、どんな非同期コードブロックも、大幅に短く、簡単に、自明にしてくれます。async/awaitは主流ブラウザとNodeJSにほぼ普遍的にサポートされているため、これらの技術を独自のコーディング手法やプロジェクトに取り入れるには、今が最高のタイミングです。

Patrick Triest
フルスタックエンジニア、データ愛好家、飽くことのない学習者、偏執的なビルダー。迷っていないふりをしながら、よく山道を歩き回っています。

https://github.com/triestpa



CREDIT:原著者の許諾のもと翻訳・掲載しています。

[原文]ASYNC/AWAIT WILL MAKE YOUR CODE SIMPLER (Posted Aug 11, 2017) by Patrick Triest
 


人気の記事

スキルアップ記事トップへ

無料サポート登録簡単30秒

【厚生労働省】職業紹介事業許可番号(13-ユ-308734)

  • STEP1
  • STEP2
  • STEP3
  • 次のstepで入力すると返事が来る!
  • プロフィール入力すると返事が来る!
  • ご希望の条件を選択してください

    ご希望の勤務形態

    必須

    ご希望の勤務地

    任意

  • プロフィールをご入力ください!必須入力項目はこのページで終わりです。

    氏名

    必須

    氏名かな

    必須

    生年月日

    必須

    電話番号

    必須

    メールアドレス

    必須

  • スキルシート・ポートフォリオをお持ちの方はアップロードしてください

    スキルシート

    任意

    提出しておくことで
    迅速なご紹介が可能に!

    職務経歴書

    ドラッグアンドドロップ or ファイルを選択 選択されていません

    履歴書

    ドラッグアンドドロップ or ファイルを選択 選択されていません

    スキルシートを確認しています...

    スキルシートを確認しています...

    ※ファイルは5MB以下で対応するファイル形式 ? でアップロードしてください
    Microsoft Office .xls .xlsx .doc .docx .ppt .pptx
    KINGSOFT Office .xls .xlsx .doc .docx .ppt
    iWork .numbers .pages .key
    LibreOffice .ods .odt .odp
    OpenOffice .ods .odt .odp
    その他 .pdf

    ポートフォリオURL

    任意

    ?

    ポートフォリオとは主にクリエイターの方が自己PRのために過去の作品や制作実績をまとめた作品集の事です。

    ポートフォリオをWeb上で公開されている方はそのURLを、データでお持ちの方は作品データをアップしたURLを入力してください。

    ※データをアップされる場合は、保存期間や容量制限の少ないGoogleドライブを推奨しています。

    その他ご要望

    任意

  • 下記の内容をご確認いただき問題ないようでしたら、送信してください

    プロフィール入力すると返事が来る!

    • ご希望の勤務形態 必須

    • ご希望の勤務地 任意

      第一希望:
      第二希望:

    • 氏名 必須

    • 氏名かな 必須

    • 生年月日 必須

    • 電話番号 必須

    • メールアドレス 必須

    • 職務経歴書 任意

    • 履歴書 任意

    • ポートフォリオURL 任意

    • その他ご要望 任意

    個人情報の取り扱い 」と「 利用規約 」に同意の上、 『同意して登録する』 ボタンをクリックして下さい。

プライバシーマーク

レバテック株式会社は「プライバシーマーク」使用許諾事業者として認定されています。
個人情報の秘密は厳守します。ご入力いただいた情報は許可を頂くまで求人企業に公開することはありませんので、ご安心ください。

申し込みに関するご注意
以下の方は弊社の事業基盤、求人動向から、ご提案のご連絡までお時間をいただく可能性があります。ご了承ください。
IT業界、または希望職種が未経験の方
レバテックキャリア対象エリア以外での勤務地、また在宅での作業を希望される方