1. miyarappo

    No comment

    miyarappo
Changes in body
Source | HTML | Preview

非同期とは何か?

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以外のモダンブラウザでは全て対応しているので、プロジェクトでもどんどん使っていきましょう!