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

【JavaScript】本日未明、[ async - await ]さんが死体で発見され...

// 全てのデータと古いデータを比較して、新しいデータだけを表示する
async function showNewData() {
  const allData = await fetchAllData();
  const oldData = await fetchOldData();
  showData(allData, oldData);
}

nanjyakorya.GIF

「な、なんじゃこりゃあああぁあっtっt!!!!」

・・・

・・・

・・・

非同期処理を"ちゃんと"理解して使いたい

『非同期処理なんとなくの理解で書いている...』

『動いてるし、ヨシ!』

令和プログラマー*1である私自身、なるべく気を付けようと思っていますが、ついついなんとなくで書いてしまいそうになります。

(*1: 令和になってからプログラミングを知った人。初心者のこと。)

ちなみに冒頭のコードは、「並列でいける処理をつい直列でやってしまっている」 例です。

実際に手を動かしながら非同期処理の理解を深める

本記事は以下の構成で、順を追って非同期処理を学習し、明日から自信を持って非同期処理が書けるようになるためのハンズオンです。

  • JSの非同期処理について知る
  • Promise について知る
  • Async / Await を使えるようにする
  • 冒頭のコードがリファクタリングできるようになる

実際に手を動かして体験することもできる内容にしています。

突然ですが問題です

以下2つの違い、分かりますか?

// ①
fetch("https://qiita.com/api/v2/items");
// ②
await fetch("https://qiita.com/api/v2/items");

さらに以下2つの違い、分かりますか?

const func = () => {
  console.log('おはよう');

  Promise.resolve('こんにちは')
    .then(res => console.log(res));

  console.log('こんばんは');
}
func();
const func = async () => {
  console.log('おはよう');

  await Promise.resolve('こんにちは')
    .then(res => console.log(res));

  console.log('こんばんは');
}
func();

分かる人には、本記事は冗長な説明になるかもしれません。

分からない人なら、本記事を読むことで
JaveScriptの非同期処理の理解が深まり、上記2つの問題に答えることができるようになるかもしれません。

JSはシングルスレッド

JSはシングルスレッドです。つまり、同時に扱えるタスクは1個だけです。
プログラムを順番に一個づつ処理します。

shopping_reji_gyouretsu_man.png

しかし、それでは困る場面もあります。時間のかかる処理が途中にあったらどうなるでしょうか。例えば...

  • 外部のDBへの問い合わせ
  • 画像処理

などは完了にどれくらい時間がかかるか分からないのです。
順番に一個づつ処理している途中で、急に処理が止まってしまい、それより後はいつ再開されるか分かりません。

そこで、『非同期』の出番です。

時間のかかる処理は一旦置いて、すぐ完了できる他の処理からどんどん実行していくことができます。

Promiseはいつか値が返ってくる約束

yubikiri_business.png
「時間のかかる処理は一旦置いて」と言いましたが、ちゃんとその処理が戻ってきてくれるのか不安です。

そこで登場するのが「Promise」です。
「DBから値を持って帰るから待っててね」と約束しているのです。

実際にAPIから値を取得してみる

では、試しにQiitaのAPIから記事データを取得してみましょう。
Chromeなどブラウザの開発者ツールを開き、コンソールで以下を実行します。

fetch("https://qiita.com/api/v2/items");

↓以下は実際にブラウザのコンソールで実行した様子です。
スクリーンショット 2020-08-21 23.34.00.png

Promiseが表示されています。
(そして<pending>とも表示されています)

中身を見てみると以下のように表示されました。
スクリーンショット 2020-08-21 23.31.15.png

Responseというものが返ってきているようです。
これはQiitaの記事を取得するAPIですが、ここではまだ記事一覧は見られないようです。

また、ステータスがfulfilled(約束が果たされた)になってます。

Promiseのステータス

Promiseにはfulfilledのようなステータス(状態)が以下の3つあります。

  • pending (保留中)
  • fulfilled (履行された)
  • rejected (却下された)

約束が果たされるのを待っている・約束が果たされた・約束が破られた
の3パターンです。

実際にPromiseを作ってみる

以下をブラウザのコンソールに打ち込み、実際にPromiseを作ることでさらに理解を深めます。

ずっと待ち続けるPromise

const p1 = new Promise(() => {});

Promiseにはコールバックを与えます。
約束が果たされた場合の処理や、果たされなかった場合の処理を書くためです。
このp1と名付けたPromiseには、空のコールバックを渡しました。

スクリーンショット 2020-08-21 23.52.09.png

p1Promiseで、pending(保留中)状態です。
約束が果たされるのを待っていますが、コールバックには何も書いていないのでいつまで経っても保留中です。

果たされるPromise

実は、Promiseのコールバックには2つの引数を設定できます。

  • resolve
  • reject

の2つです。

次は、以下のようにブラウザのコンソールに打ち込みます。

const p2 = new Promise((resolve, reject) => resolve('解決'));

コールバックの中で、resolve()を使用してみました。
スクリーンショット 2020-08-21 23.53.23.png

p2のステータスはfullfilledになっています。
また、PromiseValueの中に、resolve()に与えた引数が入っています。

破られるPromise

では今度は、reject()をコールバック内で使用します。

const p3 = new Promise((resolve, reject) => reject('棄却'));

スクリーンショット 2020-08-21 23.56.26.png

p3はステータスがrejectedになっています。

そして、コンソールには真っ赤なエラーも表示されています!
『約束が破られた』というのはエラーということです。

ここまでのまとめ①

実際にPromiseを作成することで、以下の内容が分かりました。

  • 非同期処理の返り値は、一旦Promiseにする
  • Promiseには状態がある
  • 無事に値が返ってきた -> fulfilled
  • エラーが返ってきた -> rejected

then() と catch()

Promiseに返ってきた値を扱うためにはthen()catch()という2つのメソッドが使用できます。(他にもありますがここでは扱いません)

then()catch()は、Promiseに対して使用できるメソッドで、
コールバックの中でPromiseValue(つまりPromiseに入ってきた値)を扱うことができます。

それぞれ、正常系と、異常系を担当します。

  • then()resolve()(解決)されたPromiseValueを担当
  • catch()reject()(棄却)されたPromiseValueを担当

では、先ほどのPromiseに対してthen()catch()を使ってみます。

then()の場合

p2.then(res => console.log(res));

スクリーンショット 2020-08-22 0.09.59.png

PromiseValue「解決」の文字列が出力されています。
また、新たにPromiseが返ってきています。

catch()の場合

p3.catch(err => console.log(err));

スクリーンショット 2020-08-22 0.12.05.png

PromiseValue「棄却」の文字列が出力されています。
また、こちらも新たにPromiseが返ってきています。

このように、then()catch()を用いることで、Promiseの結果にアクセスできます。

Qiita記事APIを叩いて、then()でPromiseの結果を見る

では、今度こそQiitaAPIから記事一覧を取得し閲覧してみましょう。

先ほど本資料の冒頭でQiitaAPIをfetchしたときには、Promiseだけで肝心の記事データが見れませんでした。
今度は、then()を用いてPromiseに返ってきた値にアクセスします。

(fetch("https://qiita.com/api/v2/items")).then(res => console.log(res.json()));

スクリーンショット 2020-08-22 1.50.53.png

記事情報が取得できています!

then()を繋げる

then()は繋げて書くことができます。
以下のプログラムを見てください。

getImage(file)
  .then(image => compressImage(image))
  .then(cImage => saveImage(cImage))
  .then(result => console.log(result))
  .catch(err => {throw new Error(err)})

then()がいっぱい並んでいます。
上記のプログラムは、時間のかかる画像処理をイメージしています。

  • 画像ファイルの読み込みをする
  • 画像を読み込んだら、圧縮する
  • 圧縮が完了したら、その画像をDBに保存する
  • 保存した結果を出力する

ファイルの読み込みや、圧縮、保存、といった処理はそれぞれ時間がかかるので、次の工程に進むためには、前の工程の完了を待つ必要があります。

Promise.then()を使うことで、前の処理の結果を待ち、その結果を次の処理に渡すことが可能です。

こういう処理は、通常の同期処理ではうまくいかないので、Promiseを用いた非同期処理が適しています。

また、最後のcatch()はこのプログラムのどこでエラーが発生したとしても、うまくエラーをキャッチしてくれます。
なぜかというと、then()fulfilled以外の場合は全てスルーして次の処理に渡すからです。

エラーの場合、ステータスはrejectedになっているので、全てのthen()はこれをスルーします。最後に残ったcatch()だけがこれを掴むことができます。

ここまでのまとめ②

一休みして、ここまでのおさらいをして整理しておきます。

  • JSはシングルスレッドであり、順番に同期処理がキホン
  • 時間のかかる処理や、結果が不明な処理は非同期処理したい
  • 非同期処理の結果を約束するPromiseがある
  • Promiseには非同期処理の結果が返ってくる
  • Promisefulfillされたり、rejectされたりする
  • fulfillされた場合の値は、then()で取得できる
  • rejectされた場合の値は、catch()で取得できる

Promiseが処理されるタイミング

非同期処理を扱う際は、実行タイミングに気を配る必要があるでしょう。

以下のコードを見てください。単純なログを出力するプログラムですが、console.log()出力は『こんにちは』が一番最後になってしまいます。

プログラム
const func = () => {
  console.log('おはよう');

  Promise.resolve('こんにちは')
  .then(res => console.log(res));

  console.log('こんばんは');
}
func();
出力
> おはよう
> こんばんは
> こんにちは

プログラム内でPromiseの順番は上から2番目なのに、処理は最後にされています。

コールスタックとキューとイベントループ

一体なぜPromiseのログは一番最後になったのでしょうか。

ここで登場するのが

  • コールスタック
  • キュー
  • イベントループ

という3つの概念です。

コールスタック

全ての関数は、呼び出されてコールスタックに入ります。
入った順に処理されて、処理が終わるとコールスタックから出ていきます。

スクリーンショット 2020-08-21 22.51.42.png

キュー

非同期処理も、まずはコールスタックに入ります。
ただし、非同期処理のコールバックはキューという別の場所に追加されます。

スクリーンショット 2020-08-22 1.10.30.png

イベントループ

コールスタック内の全ての処理が終了した後で、ようやくキュー内の処理の出番がやってきます。

コールスタック内の全ての処理が終了した後、キューに処理が残っている場合は、イベントループがコールスタックにその処理を追加します。

スクリーンショット 2020-08-22 1.15.11.png

このような流れになっているため、一度キューに入る非同期処理は、同期処理よりも後に実行されることになります。

Async と Await

「書いた順番通りに実行されないなんて、読みにくいし気持ちわるい」

そう思う人のために、AsyncAwaitという仕組みがJSに導入されました。

以下のようにAsyncAwaitを書き足すだけで、まるで同期処理のように順番通り出力されます。

const func = async () => {
  console.log('おはよう');

  await Promise.resolve('こんにちは')
    .then(res => console.log(res));

  console.log('こんばんは');
}
func();
出力
> おはよう
> こんにちは
> こんばんは

AwaitはAsync関数を一時停止する

一体なぜ上から順にconsole.log()出力がされたのでしょうか。

実はawaitはその時点でプログラムの実行を一旦止め、Promiseが解決されたら次に進むようにするキーワードです。

つまり、Promiseが解決されるのを待っていてくれるのです。

await以下に記述された処理は、Promiseが解決されるまで実行されません。
したがって、上から順に処理を実行することができるわけです。

QiitaAPIを今度はawait

先ほどはthen()を使ってPromiseに返ってきた値を見ることができましたが、同じことがawaitでも可能です。
awaitPromise値が返ってくるまで、待っていてくれるからです。

(await fetch("https://qiita.com/api/v2/items")).json();

スクリーンショット 2020-08-22 1.59.30.png

【注意】非同期を非同期として使おう

AsyncAwaitを使うことで、非同期処理を待って次の処理を行うことができるようになりました。

全部AsyncAwaitで同期処理っぽく書いておけば、読みやすいし扱いやすいような気がしてきます。

しかし、濫用は禁物です。非同期の本来の良さを無駄にしてしまう可能性があります。

非同期を同期処理にしてしまった例

以下のコードを見てください。
全てのデータと、古いデータを比較して、新しいデータだけを表示する関数です。

async function showNewData() {
  const allData = await fetchAllData();
  const oldData = await fetchOldData();
  showData(allData, oldData);
}

問題なのは、allDataを問い合わせている間、この関数全体が止まってしまうことです。

非同期処理をする本来の目的は、時間のかかる処理の完了を待つことなく、次の処理を行うことでした。
つまり、

  • allDataの問い合わせ と
  • oldDataの問い合わせ

は、同時に行えば効率が良いのに、awaitで処理を一旦止めてしまっているのです。

Promise.all()

複数の非同期処理をまとめ、全て完了させた後にコールバックできるAPIが用意されています。
以下のように書くことができます。

async function showNewData() {
  const [allData, oldData] = await Promise.all([fetchData(), fetchOldData()]);
  showData(allData, oldData);
}

まとめ

以下を学習しました。

  • Promise
  • then()catch()
  • asyncawait
  • コールスタックとキューとイベントループ

上記を知ることで、

  • 「なぜ」非同期処理を行うのか
  • 「どうやって」非同期処理が実現されているのか
  • 「いつ」使うのが適切か

など、なんとなくではない意図した非同期処理が少しでもできるといいなと思います。


impl_s
令和プログラマー
impl
ReactNativeのリーディングカンパニー
https://impl.co.jp/
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
ユーザーは見つかりませんでした