Help us understand the problem. What is going on with this article?

【JavaScript】非同期処理/Promise/asyncと聞くとビビっちゃう人を救う

More than 1 year has passed since last update.

非同期とは何か?

JavaScriptは非同期で実行されます。
非同期で動くとはどういうことかコードを実行して体験してみます。

sample.js
console.log('#1')

setTimeout(() => {
  console.log('#2')
}, 500)

console.log('#3')

ブラウザのコンソールかNode.jsで実行してみましょう。
Node.jsで実行するときはターミナルでnode ./sample.jsで出来ます。

#1
#3
#2

プログラムは上から順に実行されます。
同期処理では、一つ前の実行が終わるまで次の実行は待ちます。
非同期処理では、一つ前の実行に時間がかかる場合、実行完了をまたずに次の処理が行われます。

ざっくり言うと、前の反応を待つのが同期、前の反応を待たないのが非同期です。

JavaScriptは非同期で処理が実行されるため、上記のサンプルコードでは「#1 #2 #3」という順番ではなく「#1 #3 #2」という結果になったのです。

Promiseとは何か?

上記のサンプルコードを上から順番に実行するためにはどうすればいいのでしょうか?
この問題に対する解がPromiseです

Promiseを使うことで非同期言語のJavaScriptを同期的に書くことが出来ます。
「非同期処理で使うもの = Promise」ではなく「非同期処理の言語を同期的に扱いたい時に使うオブジェクト = Promise」です。

Promiseは以下のメリットを得られます。

  • コールバック地獄(もはや死語?なので詳細は割愛)を回避できる
  • 時間のかかるAPIから取得した値を次の処理に渡せる
  • 上から順に実行されるので可読性が上がる

使い方

最初のサンプルコードを数字順に表示するコードをPromiseで書いてみます。

console.log('#1')

const sample = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('#2')
      resolve()
    }, 500)
  })
}

sample()
  .then(() => {
    console.log('#3')
  })
  .catch(() => {
    console.error('error')
  })

実行すると、順番に実行されます。

#1
#2
#3

コードが冗長に感じるかもしれませんが、詳しく見ていきます。

Promiseオブジェクトの生成

Promiseを使うためには、最初にPromiseコンストラクタをnewして、初期化されたPromiseオブジェクトを生成します。
コンストラクタ引数は関数をとり、その関数の中に非同期処理を書きます。

const sample = () =>
  // 返り値にPromiseオブジェクトを生成
  return new Promise((resolve, reject) => {
      // 非同期で処理したいことを記述
      // 成功したらresolve()を呼ぶ
      resolve()
      // 失敗したらreject()を呼ぶ
      reject()
  })
}

Promiseは3種類の状態を持っています。
状態によって次に実行されるメソッドが変わります。
一度Pendingから状態が変化した後に、別の状態になることはありません。

  • Pending 初期状態
  • Fulfilled resolve()が呼ばれた時
  • Rejected reject()が呼ばれた時

次の処理に繋げる

Promiseオブジェクトは、then()catch()の2つのメソッドを持っています。
Promiseの状態がFulfilledの時はthen()が実行され、Rejectedの時はcatch()が実行されます。

sample()
  .then(() => {
    // sample()でresolve()が実行された後の処理
  })
  .catch(() => {
    // sample()でreject()が実行された後の処理
  })

thenのコールバック関数の第一引数には、Promiseでresolveに渡した値が渡ってきます。

一旦ここまでがPromiseの基本的な説明になります。

Promiseを繋げて使う

APIを叩いて値を取得するgetParamという関数を作成します。
Stringを返す架空のAPIを仮定しています。例えば/sample/fooを指定するとfooが返ります。

const getParam = url => {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    request.open('GET', url, true)

    request.addEventListener('load', (e) => {
      if (request.status === 200) {
        resolve(request.responseText)
      } else {
        reject(request.statusText)
      }
    })

    request.send()
  })
} 

Promiseは繋げて書くことが出来ます。
Promiseチェーンと呼ばれたりします。

getParam('/sample/foo')
  .then(res => {
    console.log(res)
    return getParam('/sample/bar')
  })
  .then(res => {
    console.log(res)
    return getParam('/sample/fuga')
  })
  .catch(err => {
    console.log(err)
  })

とてもきれいに書けますね!
実行結果は以下(想定)です。

'foo'
'bar'
'fuga'

並列処理

Promiseを使っていると複数の非同期処理が成功後に、何かの処理をしたい時が出てきます。

そんな時に使えるのがPromise.allです。
非同期で処理したいものを配列に入れて、全てのPromiseオブジェクトがfulfilledになった時にthenが呼ばれます。

Promise.all([
  getParam('/sample/ringo')
  getParam('/sample/gorira')
  getParam('/sample/rappa')
  getParam('/sample/pantu')
]).then(res => {
  console.log(res)
})
['ringo', 'gorira', 'rappa', 'pantu']

同様にPromise.raceは引数に配列を受けて、その中のどれか1つがfulfilledまたはrefectedになった時にthenが呼ばれます。

async/awaitを使う

PromiseチェーンによりJavaScriptはすっきりと 同期的なコードを書くこと可能になりました。
async/awaitを使うことでさらに分かりやすいコードを書くことが出来ます。

const getAsyncData = async id => {
  try {
    // idを使ってAPI経由でnameを取得
    const name = await getParam(`/sample1/${id}`)
    // nameを取得した後に、nameを使ってbelongsを取得
    const belongs = await getParam(`/sample1/${name}`)
    return belongs
  } catch(err) {
    throw err
  }
}

async/awaitを使うことで、then()やcatch()という記述をする必要がなく、より直感的に同期的な処理を実現出来ます。

ポイントは以下です。

  • awaitは、直後に記述されたPromiseオブジェクト内で処理が完了するまで処理を一時停止します。
  • awaitはPromiseの処理完了時に持っている値を取り出し変数への代入を行います。
  • awaitはasyncの中でのみ使えます。
  • 例外処理はtry-catchで行います。

まとめ

JavaScriptで分かりにくいPromiseについて解説しました。
JavaScriptはそもそも非同期で処理する言語であり、欠点を補い同期的な処理を行うためにPromiseが使われるということが理解できればPromiseは使いこなせるのではないかと思います。

async/await、PromiseはIE11以外のモダンブラウザでは全て対応しているので、プロジェクトでもどんどん使っていきましょう!

miyarappo
Software Engineer / 浅学ながらマサカリから学ぶために記事を書きます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした