7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[JavaScript] 非同期処理のPromise, async, awaitサンプル多めで丁寧にわかりやすく

Last updated at Posted at 2022-06-09

Promiseとか非同期処理が意味不明

普段プログラミングで書くのは同期処理です。
コードを順番に上から下に実行していって、途中時間のかかる処理の間は動作・画面が途中で止まることになります。

スクリーンショット 2022-06-07 20.41.08.png

例えば時間かかる処理としてはサーバーと通信して画像とか動画とかのサイズ大きいデータを取得する処理とかですね。
画像を表示しようとするたびに画面が固まって操作を受け付けないページとかあったら、イライラしてさっさと戻るボタン押しますよね。
iraira

このような場合JavaScriptで使うのは非同期処理です!
非同期処理はコードを順番に上から下に実行していくけど、時間がかかる処理は「あとで処理するよ〜」って感じで予約だけして次の処理に移らせることができます。

スクリーンショット 2022-06-07 21.08.03.png

このようにすればページ全体を表示するような処理はささっと終了させて、あとから画像が読み込まれて表示されるなんてことができますね。

処理の予約ってどうやるのか?

非同期処理で処理の予約だけして実際には処理はせずに、次に進むと書きました。
この処理の予約にはJavaScriptは関数を登録して実現します。

関数を登録(セット)して後で実行するのをコールバックと言います。
JavaScriptはこのコールバックを多用します。

  • ボタンをクリックしたときに行う処理(コールバック)をあらかじめセット。実際に押されたら処理(コールバック)が実行される
  • ファイルを選択フォームで読み込み完了した時に行う処理(コールバック)をあらかじめセット。ファイルが読み込めたら行う処理を実行

などなどイベントリスナーとして色々あります。

1. コールバックの罠、コールバック地獄

前置き長かったですのでそろそろコード書きます!
こんなコールバック普通はないと思いますけど数秒ごとに文字が出力されるコードです。

.js
const butotnClick = () => {
  setTimeout(function () {
    console.log('処理1');
    setTimeout(function () {
      console.log('処理2');
      setTimeout(function () {
        console.log('処理3');
        setTimeout(function () {
          console.log('処理4');
          setTimeout(function () {
            console.log('処理5');
            setTimeout(function () {
              console.log('処理6');
            }, 2000);
          } ,1000);
        }, 2000);
      } ,1000);
    }, 2000);
  }, 1000);
}

console.logだけだと使い道などないのでコード少し改造して一定間隔で<li>要素を追加するコードを書きましたのでよければお試しください。

See the Pen callback hell by kohishibashi (@kohishibashi) on CodePen.

これがコールバック地獄

コールバックの中に
コールバックがあって、
コールバックがその中にあって、
コードバックがさらにある、、、

という複雑な入れ子状態です。

このサンプルでも最低限の処理しか書いていないのでエラーハンドリングや途中でデータの加工などをすることになり、人間には読めたもんじゃなくなります:sob:

ここでようやく登場するのがPromise。こんな読みにくい書き方を改善するために導入されました。

2. Promise使えば入れ子構造→順番に書ける

先ほどのコードバックをもっと読みやすくしたい!ということで使われるのがPromiseです。このPromiseのthenを使うと入れ子構造を順番に書くことができます。
例えばこんな感じ

先ほどのコンソールに出力するサンプルを省略しないで忠実に書いてみるので少し長くなります💦

.js
new Promise(function(resolve) {
  setTimeout(function() {
    console.log('処理1');
    resolve();
  }, 1000)
})
.then(function() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('処理2');
      resolve();
    }, 2000)
  })
})
.then(function() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('処理3');
      resolve();
    }, 1000)
  })
})
.then(function() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('処理4');
      resolve();
    }, 2000)
  })
})
.then(function() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('処理5');
      resolve();
    }, 1000)
  })
})
.then(function() {
  return new Promise(function(resolve) {
    setTimeout(function() {
      console.log('処理6');
      resolve();
    }, 2000)
  })
})

See the Pen [Promise] callback hell by kohishibashi (@kohishibashi) on CodePen.

記述は長くはなりましたが、意味不明な入れ子構造からは卒業して上から順に実行されていくのがわかりやすくなったと思います。

  • 最初、Promiseオブジェクトを作る
  • Promiseオブジェクトは.then()を順番に繋げて書くことができる。
  • returnにPromiseオブジェクトを書いているので次もthen()を使える
  • 一つ一つ処理の終了を待って処理を進めている
  • resolve()はその関数の処理の終了させるもの
  • resolve()実行したらthen()に処理が移る

ついでにthen()の中でPromiseをreturnしない場合待たないですぐ次のthen()を実行する

2.2 冗長な書き方をリファクタリング

正直さっきのコードでは長すぎてPromise使えねーという感じなので、重複している箇所をまとめてアロー関数も使って書き直してみます。

.js
const addElementWithTimer = (msec, text) => new Promise(
  resolve => {
    setTimeout(() => {
      console.log(text);
      resolve();
    }, msec)
  }
)

addElementWithTimer(1000, '処理1')
  .then(() => addElementWithTimer(2000, '処理2'))
  .then(() => addElementWithTimer(1000, '処理3'))
  .then(() => addElementWithTimer(1000, '処理4'))
  .then(() => addElementWithTimer(1000, '処理5'))
  .then(() => addElementWithTimer(1000, '処理6'))

See the Pen [Promise2] callback hell by kohishibashi (@kohishibashi) on CodePen.

ここまで書くとコールバック地獄は消えて順番に処理していくのがわかりやすくなると思います。

3. 次の処理に引数を渡す

今回くらいの処理なら普通は使わないですが、thenで順番に処理をしていく時に次のthenの処理に値を渡したい場合があります。

※ 例えば一つ目でAPIからデータ取得して次のthenの中でデータを使って表示を変えるとか。

これは単純でresolve()に引数を与えれば次のthenに値を渡すことができます。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  resolve => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      resolve(id);
    }, msec)
  }
)

addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"

resolveの引数にidを入れることで次のthenに値を渡しています。
これを繰り返してバケツリレーのように渡すように改造してます。

※ 渡す値が同じだと面白くないのでidに1を足して渡してます。

4. async await

読み方はエイスィンク アウェイトです。

最初アスィンクと読んでいたらエイスィインクだよ。
って言われて今度はエイウェイトだと思ってたら、こっちは普通だったというどうでもいい思い出があります:innocent:

4.1 asyncを関数の前につけるとその関数はawait使える

MDNによると

非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます。 async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。

うーむわからない。
とりあえず先ほどまで出していた例を書き換えてみる

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  resolve => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      resolve(id);
    }, msec)
  }
)

const butotnClick = async () => {
  const res1 = await addElementWithTimer(1000, '処理', 1)
  const res2 = await addElementWithTimer(2000, '処理', res1)
  const res3 = await addElementWithTimer(1000, '処理', res2)
  const res4 = await addElementWithTimer(1000, '処理', res3)
  const res5 = await addElementWithTimer(1000, '処理', res4)
  const res6 = await addElementWithTimer(1000, '処理', res5)
  
  console.log(`処理終了: ${res6}`)
}

butotnClick()

これを実行すると

> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
> "処理終了: 7"

console.logの表示は↓のような埋め込みのcodepenだと出ないので、気になる人はcodepenの本体ページいってみてください。

See the Pen Untitled by kohishibashi (@kohishibashi) on CodePen.

4.2 何が変わったのか?

async関数の前におくとその関数でPromiseを返す関数は便利な書き方を使えるようになります。

.js
const sampleFunc = async function() {
// ここからawait使うと便利な書き方になる
// ...
// ...
// この関数内だけ
}
.js
// Before
const butotnClick = () => {
addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
}

// After 書いていることは同じ
const butotnClick = async () => {
  const res1 = await addElementWithTimer(1000, '処理', 1)
  const res2 = await addElementWithTimer(2000, '処理', res1)
  const res3 = await addElementWithTimer(1000, '処理', res2)
  const res4 = await addElementWithTimer(1000, '処理', res3)
  const res5 = await addElementWithTimer(1000, '処理', res4)
  const res6 = await addElementWithTimer(1000, '処理', res5)
}

4.2.2 実行順番注意

awaitをつけていない箇所の実行の順番も変わるので注意してください。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  resolve => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      resolve(id);
    }, msec)
  }
)

const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  console.log('終了')
}


const butotnClick2 = async () => {
  console.log('開始')
  const res1 = await addElementWithTimer(1000, '処理', 1)
  const res2 = await addElementWithTimer(2000, '処理', res1)
  const res3 = await addElementWithTimer(1000, '処理', res2)
  const res4 = await addElementWithTimer(1000, '処理', res3)
  const res5 = await addElementWithTimer(1000, '処理', res4)
  const res6 = await addElementWithTimer(1000, '処理', res5)
  console.log('終了')
}

butotnClick1(), butotnClick2()を実行すると

# butotnClick1() then()使ったパターン
> "開始"
> "終了"
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
# butotnClick2() async await使ったパターン
> "開始"
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
> "終了"

butotnClick1は「終了」がすぐにきています。同期処理のconsole.logは先に処理が終わっていて、非同期処理の箇所が遅れて実行されるからこうなっています。
butotnClick2は同期処理っぽく上から順番に処理してくれていてしっくりきますね。

5. Promiseのエラー処理reject

これまでは処理が成功するパターンだけを見てきました。
処理がうまくいかなかった場合に使うのがrejectです。

  • 処理成功のresolveを第一引数
  • 処理失敗のrejectは第二引数

にとります。以下は同じコードで書き方が違うだけです。

5.1 二つ目の関数を書いてエラー処理する方法

まずは関数を二つ書く書き方。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      if (id === 2) reject(id);
      resolve(id);
    }, msec)
  }
)

const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(
    id => addElementWithTimer(2000, '処理', id),  // resolveの処理
    id => { console.log(`ID: ${id} 失敗...`) }  // こっちがreject
  )
}
// 実行
butotnClick1()

id === 2の時にrejectさせます。
これを実行すると

> "開始"
> "処理1"
> "ID: 2 失敗..."

"処理2"とはならずに2個目の処理が実行されました。

引数のidを渡せるのはresolveと同じ。

5.2 catchを使う方法

どちらかといえばこちらの方が便利にかける。catchを使えばreject()されたらcatchに処理が移ります。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      if (id === 2) reject(id);
      resolve(id);
    }, msec)
  }
)
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .catch(id => { console.log(`ID: ${id} 失敗...`) })
}
// 実行
butotnClick1()

catchを追加しています。このように書いた場合2個目のthen()でrejectが起こるのでcatchに処理が移ります。
これを実行すると

> "開始"
> "処理1"
> "ID: 2 失敗..."

ついでに

一応catchの説明は以上ですが、いろんなサンプル書いてみます。文章読むよりも手を動かした方が理解早いのでこの他にも少し改造して動かしてみてください。

catchの後にthen

.js
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .catch(id => { 
    setTimeout(() => {
      console.log(`ID: ${id} 失敗...`) ;
    }, 1000);
  })
  .then(() => { console.log('これはすぐ実行される') })
}

これを実行するとcatchで1秒待ってからthenを実行してくれそうだけどすぐ実行されるから注意

> "開始"
> "処理1"
> "これはすぐ実行される"
> "ID: 2 失敗..."

次のthenに移るためには、Promiseオブジェクト返すのと、resolve()しないといけないのでこのように書く必要がある。

.js
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .catch(id => { 
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(`ID: ${id} 失敗...`) ;
        resolve();
      }, 1000);
    });
  })
  .then(() => { console.log('Promiseが欲しかった') })
}

return省略できる書き方もあるけどわかりやすさ優先で書きました。

> "開始"
> "処理1"
> "ID: 2 失敗..."
> "Promiseが欲しかった"

関数二つ使ったパターンにcatchも使う

引数二つとcatchを同時に使うとcatchは使用されない。

.js
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(
    id => addElementWithTimer(2000, '処理', id),  // resolveの処理
    id => { console.log(`ID: ${id} 失敗...`) }  // こっちがreject。これが使われる
  )
  .catch(id => { console.log(`catch実行されないよ`)  })
}
> "開始"
> "処理1"
> "ID: 2 失敗..."

6. Promiseの成功しても失敗しても実行するfinally

これまで処理が成功、失敗する場合のPromiseの書き方を見てきました。
実はもう一つあって、成功しても失敗しても実行するfinallyというものがあります。

finallyを使ってみる

resolve()されて終了しても、reject()されて終了しても最後に処理を行ってくれるfinallyの例を見ていきます。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      if (id === 2) reject(id);
      resolve(id);
    }, msec)
  }
)
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .catch(id => { console.log(`ID: ${id} 失敗...`) })
  .finally(()=> { console.log('処理終了!') })
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"

処理が失敗して最後にfinallyの処理が実行されて終了してますね。

次にrejectを消して、catchにならないように実行してみると

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      resolve(id);
    }, msec)
  }
)
const butotnClick1 = () => {
  console.log('開始')
  addElementWithTimer(1000, '処理', 1)
  .then(id => addElementWithTimer(2000, '処理', id))
  .then(id => addElementWithTimer(1000, '処理', id))
  .catch(id => { console.log(`ID: ${id} 失敗...`) })
  .finally(()=> { console.log('処理終了!') })
}
> "開始"
> "処理1"
> "処理2"
> "処理3"
> "処理終了!"

thenが全部終わって、最後にfinallyの処理が実行されて終了してますね。
用途としては処理の後処理なんかに使えます。例えば読み込み中のグルグルと回る「通信中表示」を消すとか。

7. async awaitのエラー処理rejectはtry catch

thenにcatchがあるならasync awaitを使った書き方にも当然catchがあります。
コードはthencatchを繋げて書いたときの内容と同じく、idが2になったらreject。引数にidを渡すコード。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      if (id === 2) reject(id);
      resolve(id);
    }, msec)
  }
)

const butotnClick1 = async () => {
  console.log('開始')
  try {
    res1 = await addElementWithTimer(1000, '処理', 1);
    res2 = await addElementWithTimer(1000, '処理', res1);
    res3 = await addElementWithTimer(1000, '処理', res2);
  } catch(e) {
    console.log(`ID: ${e} 失敗...`)
  }
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."

同期処理書く感じで書けるので超わかりやすいですね!

8. async awaitでfinally

これは説明するまでもないかもしれませんが一応書きます。
さっきの例と同じく、idが2の時にrejectされるのでcatchに入り、最後にfinallyに入ります。

.js
const addElementWithTimer = (msec, text, id) => new Promise(
  (resolve, reject) => {
    setTimeout(() => {
      console.log(`${text}${id}`);
      id++
      if (id === 2) reject(id);
      resolve(id);
    }, msec)
  }
)

const butotnClick1 = async () => {
  console.log('開始')
  try {
    res1 = await addElementWithTimer(1000, '処理', 1);
    res2 = await addElementWithTimer(1000, '処理', res1);
    res3 = await addElementWithTimer(1000, '処理', res2);
  } catch(e) {
    console.log(`ID: ${e} 失敗...`)
  } finally {
    console.log('処理終了!');
  }
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"

async awaitだと素直に同期処理っぽく書けるのでfinally書く必要あることがあまりないかもしれないですね。わざわざfinally書かなくても下のように書けます。

.js
const butotnClick1 = async () => {
  console.log('開始')
  try {
    res1 = await addElementWithTimer(1000, '処理', 1);
    res2 = await addElementWithTimer(1000, '処理', res1);
    res3 = await addElementWithTimer(1000, '処理', res2);
  } catch(e) {
    console.log(`ID: ${e} 失敗...`)
  }

  console.log('処理終了!');
}
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"

最後に

ここまでやればとりあえずコードを解読することはできるのではないかと思います!

これが理解できたら静的メソッドも勉強してみたらPromiseは大体問題ないかなと思います!

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?