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

【図解】1から学ぶ JavaScript の 非同期処理

Ateam Lifestyle Advent Calendar 2020の15日目は 株式会社エイチームライフスタイルの @ryosuketter が担当します。

はじめに

JavaScriptで非同期処理を書くシーンは数多くあると思います。

なのに、今までなんとなく使用してきました。これを機会にちゃんと勉強したいと思い体系化してまとめました。

それだけだとタダのメモになってしまうので、なるべく初学者の人が理解しやすいように書きました。

自分はこの記事を書くことで、JavaScriptの躓きポイントの代表格である非同期処理(Promise や async/await )についても理解が深まったのでいい内容だと思ってます。長いけど。

主に初学者の方に読んでいただけたら幸いです。

JavaScript上級者の方は。何か間違っている記載があれば是非コメントください。

同期処理と非同期処理

まずは用語の定義からです。

同期処理は、最初のコードから次のコードへと順次処理(実行)されていくことです。

対して、非同期処理とは、ある処理が終了するのを待たずに、別の処理を実行することです。

同じような説明を後述しますが、ここではシンプルに説明しました。

JavaScriptには、なぜ非同期処理が必要なのか

同期処理だと、時間がかかる処理が終了するまで次の処理を行えないと効率が悪いため、非同期処理が対策としてあります。

家事で例えれば、洗濯が終わるまで、家の掃除をしないと聞くと、極めて非効率のように感じますよね。

しかし、あるサーバからネットワークを介してデータ取得されるのを待ってから、簡単な四則演算を開始する。この処理も、条件によっては同期的な処理でも人の感覚だと違和感がないほど一瞬かもしれません。ですが、ネットワークを介したデータ取得と、ローカルのPC内だけで完結するような四則演算ではコストが大きく異なります。

(それを計算で求めてる記事を昔読んだのですが、忘れてしまいました。。)

コストが大きく異なる2つの処理を同期的に順次処理していくのは効率が悪いので、その対策として非同期処理が必要になりました。

次はスレッドという用語の説明をします。

スレッドとは

スレッドとは、連続して順番に何かしらの処理が実行される流れのことです。

スレッド(thread)とは英語で糸という意味だそうです。

IMG_8F66BD910919-1.jpeg

JavaScriptを実行するブラウザには、いろんなスレッドがあります。

  • メインスレッド
  • サービスワーカー
  • Webワーカー

主にJavaScriptが実行されるのは、メインスレッドだと思います。

メインスレッドでは何が行われているのか?

ブラウザで、JavaScriptは以下の2つの仕事をメインスレッドで行っています。

  • JavaScriptのコードの実行
  • 画面への描写(レンダリング)

IMG_11384C465C9D-1.jpeg

JavaScriptの実行の結果、レンダリングが必要ならレンダリングが行われる仕組みになります。

少し話は変わりますが、あるWebサイトをスクロールをしてカクカクするWebサイトと、カクカクしないなめらかに動くWebサイトがありますよね?

この違いを説明するために、まずFPSから説明します(脇道に逸れているようで、逸れていませんのでご安心ください)。

FPS(Frame Per Second)は1秒間に何回画面が切り替わるかの単位です。

MDN曰く

60fps のフレームレートがなめらかなパフォーマンスの目標値であり、あるイベントに対して必要なすべての更新に与えられた時間は 16.7 ミリ秒です。

フレームレート - 開発ツール | MDN

60FPSなので、1秒間に60回画面が切り替わればヒトには、カクカクしない滑らかな画面描写といえる。ということです。

16.7という数値は以下の式が元になっています。

1,000(ms) / 60(times) = 16.7(ms)

つまり、カクカクして見えるとは、メインスレッドが16.7(ms)以上かかっていると言えるでしょう(カクカクの感じ方には個人差があります)。

IMG_AB124E256746-1.jpeg

ちなみに、任天堂Switchの最大fpsは60です。

最後に、直感的に理解できるように、iPad のリフレッシュレート(60 と 120Hz)を比較した動画があるので、 是非見てみてください。

iPad Pro 10.5-inch ProMotion 60 vs 120Hz Latency Test with Apple Pencil - YouTube

次に、メインスレッドと非同期処理の関係について説明していきます。

メインスレッドと非同期処理の関係

まず、大事なポイントとして、JavaScriptはシングルスレッドの実装しかできません。

つまり、2つ以上の処理を並行して実行できないということです。

非同期処理はメインスレッドではなく別のスレッドで実行しているイメージがあるかもしれませんが、基本的にはメインスレッドで実行されています。

その様子を次で図解します。

同期処理のイメージ

非同期処理を実装せずにプログラムを書くと、このイメージのように順次実行されます。

IMG_5322391BEBF6-1.jpeg

非同期処理のイメージ

非同期処理を実装すると、その処理はメインスレッドの並びから離れて次の処理に譲ります(そんなイメージ)。

IMG_89FE469CD4E2-1.jpeg

代表的な非同期処理(非同期処理API)は、PromisesetTimeout などです。

次に、実際にsetTimeoutを用いた、非同期処理を書いていきます。

setTimeoutを用いた非同期処理

setTimeoutのシンタックス
setTimeout('コールバック関数', 'タイムアウト時間(ms)')
console.log(1);
setTimeout(() => console.log(2), 5000);
console.log(3);

実行結果

1
3
2

基本的にはJavaScriptはシングルスレッドの実装しかできないので、1 -> 2 -> 3という実行結果と思いきや1 -> 3 -> 2になります。
なぜなら、setTimeoutという非同期処理APIがメインスレッドから一時的に離れて、5秒後に再びメインスレッドで実行されているからです。

続いて、クリックイベントを用いた実装をみてみましょう。

HTMLの記述
<!DOCTYPE html>
<html>
  <head>
    <title>test</title>
    <meta charset="UTF-8" />
  </head>

  <body>
    <button>button</button>
    <div id="app"></div>

    <script src="src/index.js"></script>
  </body>
</html>

JavaScriptの記述

const sleep = (second) => {
  const startTime = new Date();
  while (new Date() - startTime < second);
  console.log("done sleep");
};

const button = document.querySelector("button");

button.addEventListener("click", () => console.log("clicked"));

sleep(5000)

これを実行して、console.log("done sleep")される5秒間の間にbuttonを3回クリックした実行結果は

done sleep
(3) click

となります。これは同期的な処理なので**sleep関数が完了する5秒間の間ボタンクリック時のコールバック関数の実行結果は待ちの状態になっています。

ですが、sleep関数を、setTimeout(非同期処理API)の中のコールバック関数として扱うと、

setTimeout(() => sleep(3000), 2000);

setTimeoutは非同期的な処理なので第二引数で指定した2秒間はメインスレッド中をクリックイベントで割り込める時間にできます。

IMG_0BB0184B4DC2-1.jpeg

割り込んでるイメージは伝わりましたか?

ここまでわかると、今度は、「メインスレッドの外から一時的に離れるってどこいくのだろう?」とか思いませんか?

それを理解するためには、イベントループのメカニズムを理解する必要があります。

イベントループのメカニズム

JavaScriptエンジンやブラウザには、以下の4つのメカニズムを備えています。

  • Web API(例 DOM Event や setTimeout など)
    • これらは非同期に実行されます
  • ヒープ(Heap)
    • 動的に確保と解放を繰り返せるメモリ領域で、オブジェクトはヒープに割り当てられます
    • JavaScriptエンジンの内部に実装されています
  • コールスタック(Call Stack)
    • 関数は呼び出されるとコールスタックに追加されます
    • メインスレッドが占有されている状態はコールスタックにコンテキストが積まれている状態とも言えます
      • コールスタックにコンテキストが積まれている時は、タスクキューは待ちの状態で、コールスタックにあるコンテクストがはけるまではタスクは処理されません
    • また、コールスタックは、LIFO(Last In First Out)の構造を持った領域です
    • JavaScriptエンジンの内部に実装されています(メインスレッド)
  • タスクキュー(Task Queue)
    • 実行待ちの非同期処理の行列のことを指します。別の言い方をすれば、非同期処理の実行順序を管理しているとも言えます
    • 非同期処理はタスクキューに入った順番で処理は実行されます
    • また、タスクキューは、FIFO(First In First Out)の構造を持った領域です
    • JavaScriptエンジンの外部に実装されています(メインスレッド外)

IMG_57D98421C8A1-1.jpeg

イベントループやタスクキューや、Web APIはJavaScriptの機能ではなくブラウザが提供しているものです。

こちらの願念理解は、文字で説明するよりも👇 の動画の方がわかりやすいで見てみてください。

What the heck is the event loop anyway? | Philip Roberts | JSConf EU - YouTube

もしくは、GIFアニメで分かりやすく解説している記事も理解しやすかったです。

✨♻️ JavaScript Visualized: Event Loop - DEV

ここまで概念で説明してきたイベントループを、次は実例を交えて説明していきます。

コールバック関数と非同期処理

コールバック関数を用いた非同期処理は実行順序を理解する上で是非覚えておくといいと思います。

下記の処理を見てください。

const first = () => {
  setTimeout(() => console.log("task1 done"), 2000);
  console.log("function first done");
};

const second = () => console.log("function second done");

first();
second();

実際の実行結果は以下のような感じです。

function first done 
function second done 
task1 done 

順序をリストで表すと

  • スクリプトが実行される
  • コールスタックにグローバルコンテクストから関数first()が実行される
  • 関数first()が実行されると、WEB API(非同期APIのsetTimeout())が実行される
  • WEB API(非同期APIのsetTimeout())が実行されると、タスクキューの中にsetTimeout内のコールバック関数が登録される
  • 関数first()の実行が終了すると、グローバルコンテクストから関数first()がpopされる
  • グローバルコンテクストから関数second()が実行される
  • グローバルコンテクストから関数second()がpopされる
  • グローバルコンテクストが空になる
  • 空になったことをイベントループがタスクキューに伝える
  • タスクキューに最初に入った順(今回はsetTimeout内のコールバック関数だけ)実行される

文字だけではわかりにくいですよね。。。わかりにくければ動画も作成したので、見てみてください。

「補足資料」JavaScript の 非同期処理 の様子をイベントループのメカニズムの図を用いて解説 - YouTube


では、以下のような出力結果にしたいときは、どう実装すればいいでしょうか?

function first done 
task1 done 
function second done 

こんな感じです。

const first = () => {
  setTimeout(() => {
    console.log("task1 done");
    second();
  }, 2000);
  console.log("function first done");
};

const second = () => console.log("function second done");

first();

順序をリストで表すと

  • スクリプトが実行されると、コールスタックにグローバルコンテクストから関数first()が実行される
  • WEB API(非同期APIのsetTimeout())が実行される
  • setTimeoutの中にはconsole.logの後に、関数second()がある
  • グローバルコンテクストから関数first()がpopされる
  • グローバルコンテクストが空になる
  • 空になったことをイベントループが伝える
  • タスクキューに最初に入った順(console.logの後に、関数second()実行される

こんな感じです。

ただ、実際の業務においてはもっと複雑なコードを書いていると思います。

非同期処理を決められた順序で実行したいときも多いと思います。

次はそんな方法の1つとして、非同期処理のチェーンを紹介します。

非同期処理のチェーン

複数の非同期処理をコールバック関数を使って連続的につなげて処理する方法を紹介します!

0秒から+1づつカウントアップさせたい場合、以下のように書きます。

const sleep = (callback, val) => {
  setTimeout(() => {
    console.log(val++);
    callback(val);
  }, 1000);
};

sleep((val) => {
  sleep((val) => {
    sleep((val) => {
      sleep((val) => {
        sleep((val) => {}, val);
      }, val);
    }, val);
  }, val);
}, 0);

....ネストが深すぎますね。。

コールバック関数の中に、コールバック関数を実行させる方法で変数valをカウントアップさせています。

これが、コールバック関数を用いた複数の非同期処理を順番に実行(非同期のチェーン)するための方法です。

問題

これで、実行順序を確かに制御できますが、問題は可読性が悪いことです。
この問題を解決するために、ES6ではPromiseというオブジェクトが生まれました。

Promise とは

Promiseとは、非同期処理をより簡単かつ可読性が上がるように書けるようにしたJavaScriptのオブジェクトです。

Promise の状態

Promiseには3つの状態があります。

pending:非同期処理の実行中の状態を表す
fulfilled:非同期処理が正常終了した状態を表す
rejected:非同期処理が異常終了した状態を表す

Promise の書き方

一般的なPromiseの書き方は下記です。

new Promise(
  // something
).then(
  // something
).catch(
  // something
).finally(
  // something
);

Promiseの引数としてコールバック関数を設定します。
↓のコールバック関数は引数を2つとります。

  • resolve
  • reject
new Promise((resolve, reject) => {
}).then(
  // something
).catch(
  // something
).finally(
  // something
);

resolve

Promiseの状態が、fulfilledになったら resolve が実行されます。
resolve が実行された場合は、thenメソッド内部が実行されます。
thenメソッド内部のコールバック関数には、resolve実行時の実引数が渡されます。それが("hoge")です。

new Promise((resolve, reject) => {
  resolve("hoge");
}).then((data) => console.log(data)  // => "hoge"
).catch(
  // something
).finally(
  // something
);

thenメソッドが実行された場合、catchメソッドは無視されます。
最後にfinallyメソッドが実行されます。

new Promise((resolve, reject) => {
  resolve("hoge");
}).then((data) => console.log(data) // => "hoge"
).catch(
  // something
).finally(() => console.log("finally"));

reject

では、rejectが実行された場合はどうでしょうか?

Promiseの状態が、rejectedになったら reject が実行されます。

rejectと言うのは、Promise内のコールバック実行中に何らかのエラーが発生した場合、それをPromiseに通知するために使用する関数です。

rejectが実行される場合は、cathchメソッド内のコールバック関数が実行されます。promise内にある、rejectの実引数が渡され、実行されます。

そして、catchメソッド内のコールバック関数が実行された後に、finallyメソッド内のコールバック関数が実行されます。

new Promise((resolve, reject) => reject("fuga"))
    .then()
    .catch((data) => console.log(data)) // => "fuga"
    .finally(() => console.log("finally")); // => "finally"

Promiseオブジェクト内の同期・非同期関係は以下のようになっています。

new Promise((resolve, reject) => {
  // 同期処理
}).then(() => {
  // resolveの実行を待って非同期処理
}).catch(() => {}
  // rejectの実行を待って非同期処理
}).finally({
  // resolveかrejectの実行を待って非同期処理
});

Promiseを実際に書いていきます

new Promise((resolve, reject) => {
  console.log("Promise");
  resolve();
}).then(() => console.log("then"));

console.log("global end");

実行結果

Promise
global end
then

2番目に global end が出力されていることがわかります。
これはなぜかと言うと、thenメソッド内部は非同期処理なので、タスクキューに積まれたからです。あと、グローバルコンテキスト上にあるconsole.log("global end")がコールスタックに2番目に積まれているからです。
それが実行されれば、コールスタック(メインスレッド)が空になります。そのタイミングでthenメソッドが実行されます。

new Promise((resolve, reject) => {
  console.log("Promise");
  resolve();
})
  .then(() => {
    console.log("then1");
  })
  .then(() => {
    console.log("then2");
  })
  .then(() => {
    console.log("then3");
  });

console.log("global end");

実行結果

Promise 
global end 
then1 
then2 
then3

いくら繋いでも、多階層にならないのでコードの可読性が保たれます。

非同期処理のチェーンと違って、Promiseは非同期処理のチェーンを書くのに優れていることがわかります。

catchメソッドは以下のような感じです。

new Promise((resolve, reject) => {
  console.log("Promise");
  reject();
}).catch(() => {
  console.log("catch");
});

console.log("global end");

実行結果

Promise 
global end 
catch

特定のthenの中でエラーを補足したい場合

そんな時は、以下のように書きます。

new Promise((resolve, reject) => {
  console.log("Promise");
  resolve();
})
  .then(() => {
    console.log("then1");
    throw new Error(); // エラーインスタンスをわざと発生させています。そのエラーをthrowが捕まえて、catchに処理を移行させています。
  })
  .then(() => {
    console.log("then2");
  })
  .catch(() => {
    console.log("catch");
  });

console.log("global end");

実行結果

Promise 
global end 
then1 
catch

then2 が呼ばれていないことがわかりますね!

then メソッドのコールバックに引数を設定するパターンを見てみましょう。

new Promise((resolve, reject) => {
  console.log("Promise");
  resolve("hoge");
})
  .then((data) => {
    console.log(`then1 - ${data}`);
  })
  .then((data) => {
    console.log(`then2 - ${data}`);
  })
  .catch((data) => {
    console.log(`catch - ${data}`);
  })
  .finally((data) => {
    console.log(`finally - ${data}`);
  });

console.log("global end");

実行結果

Promise 
global end 
then1 - hoge 
then2 - undefined 
finally - undefined 

finally は引数が渡せない仕様になっています。なので、undefined

ですが、 then2 も、undefindになっていますね。

then2にもデータを渡したい場合は、then1部分にreturn を書けば渡せます。

new Promise((resolve, reject) => {
  console.log("Promise");
  resolve("hoge");
})
  .then((data) => {
    console.log(`then1 - ${data}`);
    return data;
  })
  .then((data) => {
    console.log(`then2 - ${data}`);
  })
  .catch((data) => {
    console.log(`catch - ${data}`);
  })
  .finally((data) => {
    console.log(`finally - ${data}`);
  });

console.log("global end");

実行結果

Promise 
global end 
then1 - hoge 
then2 - hoge 
finally - undefined 

rejectの方も似たような感じなので、省略します。

Promiseオブジェクトで大事なこと

  • Promise内のresolveメソッドが実行されるまで、then()の中身は実行されない
  • Promise内のrejectメソッドが実行されるまで、catch()の中身は実行されない

です。

この特徴を利用して、Promiseのコールバックに非同期処理を組み込むことができます。

new Promise((resolve, reject) => {
  console.log("Promise");
  setTimeout(() => {
    resolve("hoge");
  }, 1000);
})
  .then((data) => {
    console.log(`then1 - ${data}`);
    return data;
  })
  .then((data) => {
    console.log(`then2 - ${data}`);
  })
  .catch((data) => {
    console.log(`catch - ${data}`);
  })
  .finally((data) => {
    console.log(`finally - ${data}`);
  });

console.log("global end");

実行結果

Promise 
global end 
then1 - hoge 
then2 - hoge 
finally - undefined

次は、Promiseを使って、非同期処理を順番に実行する方法を紹介します。

Promiseのチェーン

読んで字の如くですが、Promiseのチェーンとは、Promiseを使って、非同期処理を順次実行することです。

非同期処理のチェーンの項目で、複数の非同期処理をコールバック関数を使って連続的につなげる方法を紹介しました。👇

これのデメリットは、階層が深くなりやすく可読性が落ちる点です。

const sleep = (callback, val) => {
  setTimeout(() => {
    console.log(val++);
    callback(val);
  }, 1000);
};

sleep((val) => {
  sleep((val) => {
    sleep((val) => {
      sleep((val) => {
        sleep((val) => {}, val);
      }, val);
    }, val);
  }, val);
}, 0);

これをPromiseを使って実装してみましょう。関数 sleep の定義を変えていきます。

  • まず、new Promiseで、Promiseのインスタンス化をする
  • Promise内のコールバック関数内の引数はいつも通り、resolveとreject(今回はrejectは使わないので第二引数は省略)
  • setTimeout(関数, 待ち時間)Promise()の中に入れる
  • setTimeout内の callback(val)resolve(val) に書き換える
  • sleep関数の第一引数のcallback引数は削除する(関数を渡す必要はないので)
  • sleepの呼び出し元に返却するため new Promise の結果をreturnする。そうすることで、sleep実行の際にthenメソッドが使える

sleep関数の定義

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

sleep関数の実行(1)

sleep(0).then(sleep).then(sleep).then(sleep).then(sleep);

☝️と同じですが、こんな書き方もできます。

sleep関数の実行(2)

sleep(0)
  .then((val) => sleep(val))
  .then((val) => sleep(val))
  .then((val) => sleep(val))
  .then((val) => sleep(val));

実行結果

0
1
2
3
4

1秒ごとに増えていきますね。

slee(0)の後に、thenメソッドが使えるのは、sleep関数はPromiseオブジェクトをリターンしているからです。

疑問に思う方はconsole.logして確認してみてください。

Promiseのチェーンをつなげる際、thenメソッド内のコールバック関数に、return書いて、sleep関数を実行して引数はvalとしています。

そうすることで、Promise内の処理が再度実行され、 その結果がthenメソッド内のコールバック関数内のreturnにセットされます。

アロー関数を使って省略して書いてるので、ちょっと読むのが大変かもしれませんが、そこは慣れてください。

大事なポイント

非同期の処理をつなげるためには、thenメソッド内のコールバック関数のreturnにPromiseのインスタンスをセットすることを覚えておきましょう。

return を書かないと、後続の処理が待たずに実行されてしまうので、チェーンになりません。

もちろん、実行結果は、非同期処理のチェーンの項目で、複数の非同期処理をコールバック関数を使って連続的な処理をする方法の時と同じです。

次に、Promiseオブジェクトができることをもう少し紹介していきます。

Promiseオブジェクトを使った並列処理

Promiseのチェーンの章では、処理を直列で記載しましたが、今回は並列で処理する方法を紹介します。

  • Promise.all => 並列処理してる全てが完了したら後続処理に続く
  • Promise.race => 並列処理してる処理のどれかが完了したら後続処理に続く
  • Promise.allSettled => rejectなど、エラーがあった際にはallの場合にはcatchメソッドにいくが、コイツはobjectのstatusに持たせている

Promise.all

Promise.allだけ実例を紹介します。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

Promise.all([sleep(0), sleep(1), sleep(2)]).then(() => console.log("end"));
0
1
2
end 

endの出力は、[sleep(0), sleep(1), sleep(2)]の全処理の完了を待っていることがわかります。

Promise.allで大事なのは、反復可能オブジェクト(今回なら配列[])の中に、Promiseのインスタンスを返すと言うことを覚えておきましょう。

次、ここまで説明できたらやっと説明できる用語の解説だけさせてください。

Macrotasks(マクロタスクス)とMicrotasks(マイクロタスクス)

  • マクロタスクスは、これまで、タスクキューと呼んでいたもの
    • ここにアサインされる関数: setTimeout
  • マイクロタスクス(別名: ジョブキュー)は、タスクキューとは別で存在する非同期処理の待ち行列
    • ここにアサインされる関数: promise

それぞれの挙動の違いは少しあって、

IMG_FF37DA1977AE-1.jpeg

コールスタックがなくなると、次はマイクロタスク中にある処理が実行され、それが全てなくなるとマクロタスク中にある処理が実行されるということ。

一回、マイクロタスクのキューが全てなくなって、マクロタスクにある処理が実行された最中に、マイクロタスクからキューが追加されたら、また、マイクロタスクにあるキューがなくならない限り、マクロタスクにある処理は実行されません。そう言う点が少し異なる点です。

AsyncとAwait

AsyncとAwaitは、Promiseをさらに直感的に書けるようにしたものです。

Async

Asyncを使って宣言された関数は、Promiseオブジェクトを返却します。

つまり、関数宣言の先頭に、Asyncがついていたら、Promiseオブジェクトがreturnされることを担保しているよ。
と言うことになります。

Asyncが返すものは、Promiseなので、thenメソッドをつなげられます!。

注意点としては、Asyncは関数コンテクストにしか使えないところです。

Await

Awaitは、Promiseを返却する関数の非同期処理が終わるのを待つ(制御する)記述です。

Awaitで受けられるものは、Promiseのインスタンスです。

関数の中でawaitが記載されている場合、必ずasyncを関数の先頭に書かないとエラーなります。

AsyncとAwaitを実際に書いていきましょう

Promiseのチェーンで書いたコードを元に書き換えていきましょう。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

sleep(0).then(sleep).then(sleep).then(sleep).then(sleep);

この処理をさらに簡略化して書けます。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(val);
    }, 1000);
  });
};

async function init() {
  console.log(await sleep(0));
}

init();

まず、await sleep(0)のconsole.logによる出力結果は、0でした。

これはどう言うことかと言うと、Promise内の、resolveの引数(今回は、val)が渡されたと言うことになります。

ちなみに、 awaitをつけないと、Promiseオブジェクトそのものが渡されます。

awaitに返り値(resolveの引数)が渡ってくるタイミングは、Promise内のresolveが呼ばれるまで待機状態になります。

それでは、チェーンを使って繋いでいきましょう。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

const init = async () => {
  let val = await sleep(0);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  console.log("aaaa");
};

init();

Asyncをつけた関数はPromiseを返します。

console.logh(init()); // => Promise {<pending>} <constructor>: "Promise"

Promiseを返すので、initに後述して、thenメソッドをつなげて書けます。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

const init = async () => {
  let val = await sleep(0);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  console.log("aaaa");
};

init().then(() => console.log("sssss"));

init関数の実行を待って、thenメソッド内の処理(今回なら、console.log)が実行されていることがわかります。

523b6b45ebe01a6a5d6e7c185f07e1e5.gif

init()の戻り値にvalを設定すると、then メソッドのコールバック関数の引数に渡ってきます。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

const init = async () => {
  let val = await sleep(0);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  return val;
};

init().then((val) => console.log(`sssss${val}`));

結果

0
1
2
3
4
sssss5 

そして、Async関数内で throwが呼ばれたら、init()のcatchに処理が移ることはPromiseの項目で説明した通りです。

const sleep = (val) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(val++);
      resolve(val);
    }, 1000);
  });
};

const init = async () => {
  let val = await sleep(0);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  val = await sleep(val);
  throw new Error();
  return val;
};

init()
  .then((val) => console.log(`sssss${val}`))
  .catch((e) => console.error(`error is ${e}`));
0
1
2
3
4
error is Error

これが基本的な、Async, Awaitの書き方です。

キーワード的には、Async, Awaitですが、内部的には、Promiseと同じなので、Promiseの理解が大事です。

大事なポイント

  • asyncが返すものは受けられるものは、Promiseオブジェクトそのもの
  • awaitで受けられるものは、Promiseのインスタンス

と言うことは、覚えておきましょう。

fetchという関数

fetchという関数を使うことで、サーバからデータを取得したりできます。

使い方

fetch("リクエスト先のURL");

では、何か取得してみましょう。今回は、Qiita API v2 を使って自分のQiitaの投稿一覧を取得します。

  • Qiita API GET/api/v2/users/:user_id/items のリクエスト
  • レスポンスから、指定されたユーザの投稿一覧を取得できる

公式ドキュメント: Qiita API v2ドキュメント

注意 利用制限 を読んでおくといいです。

認証している状態ではユーザごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。

console.log(fetch("https://qiita.com/api/v2/users/ryosuketter/items"))

を、実行すると、Promiseオブジェクトが返っていきます。Promiseオブジェクトと言うことは、thenが使えますね。

thenのコールバック関数の引数(response)の中身を見てみます。

fetch("https://qiita.com/api/v2/users/ryosuketter/items").then((response) => console.log(response));

結果は👇

スクリーンショット 2020-11-28 21.13.48.png

レスポンスの中身は、サーバから返ってきたデータの中身がオブジェクトになっています。

例えば、ok: trueというのは、サーバからデータが取得できたと言う意味です。status: 200はHTTP status code のことです。

オブジェクトの中にある、json()と言う関数を使ってみましょう。

thenの中で json形式でレスポンスした内容をreturnさせます。これを①とします。

次に繋がれたthenの中身のコールバック関数の引数には①が渡されます。

fetch("https://qiita.com/api/v2/users/ryosuketter/items")
  .then((response) => {
    return response.json();
  })
  .then((json) => {
    console.log(json);
  });

中身を確認すると、リクエストしたAPI経由からきたデータが、JSのオブジェクトと配列で変換されたjsonという形式で渡ってきたとわかります。

531adf5e59e25c7e92cb9b75d41ab50b.gif

業務だと、このデータの形式で何らか加工してWebページに表示させることが多いです。

大事なポイント

今回大事なポイントは2つです。👇

dreamy-proskuriakova-wfo0l_-_CodeSandbox.png

では、最後にここまでの記述を async / await で記述してみましょう。

まず、 asyncを利用するためには、関数として実行する必要があるため fetchQiitaItems() という関数を作成します。

次に、fetchの戻り値を awaitで受けます。 fetch の戻り値は Promiseなので、awaitで受けることで処理の完了を待つことができます。そんで、それを変数(response)に格納します。

次に、変数 json を作成して、response.json()のデータを格納してあげます。response.json()レスポンスも中身はPromiseなので、awaitで受けてあげましょう。

こんな感じになりました。こう書けば、先ほどと同じ結果になります。

const fetchQiitaItems = async () => {
  const response = await fetch(
    "https://qiita.com/api/v2/users/ryosuketter/items"
  );
  const json = await response.json();
  console.log(json);
};

実際の業務の場合も、どのようなオブジェクトで返ってきているかわからない場合は、console.logで確認しながら開発していくのが大事かなと思います。

例外処理とエラー

まず、例外処理についての説明です。例外処理とはエラーが発生した際に発火する特別な処理です。

例外処理は、try、catch、finallyのブロックでそれぞれ処理されます。

try {
  throw new Error();
} catch (e) {
  // エラーハンドリング
} finally {
  // 終了処理
}

tryのブロック内でエラーが発生した場合は、それ以降のコードは実行されず、catchのブロックに移行します。
そして、finallayは、tryまたは、catchの後に必ず実行される処理を記載します。

では以下に、基本的な try、catch、finally の使い方を見ていきましょう。

try {
  console.log(1);
  throw new Error();
  console.log(2);
} catch (e) {
  console.error(e);
} finally {
  console.log("finally");
}

console.log(2)が実行されていないことがわかります。

dreamy-proskuriakova-wfo0l_-_CodeSandbox.png

もう少し実践的な方法をしてみます。

引数に文字列でQiitaのユーザーネームを指定すれば、APIを経由して、該当ユーザーがいれば、データを返し、なければエラーを吐く関数です。

const fetchQiitaItems = async (userName) => {
  try {
    const response = await fetch(
      `https://qiita.com/api/v2/users/${userName}/items`
    );
    if (response.ok) {
      const json = await response.json();
      return json;
    } else {
      throw new Error("not fount");
    }
  } catch (e) {
    console.error(e);
  }
};

Qiitaのユーザーネームが存在する場合

console.log(fetchQiitaItems("ryosuketter"));
// => Json形式で何かしらデータが返ってきます

Qiitaのユーザーネームが存在しない場合

console.log(fetchQiitaItems("ryosuketterrrrrrrrrrrrr"));
// => Error: not fount

fetchQiitaItems関数内にある if文にて、データのあるなしを判定しています。

変数、responseはPromiseオブジェクトです。そのなかに、boolを管理しているokというデータがありましたね。その内容次第でだし分けています。

さいごに

なお、本記事は私が調べた範囲の情報のみになりますので誤情報が含まれるかもしれません。その際はぜひ編集リクエストをいただけるとありがたいです。

参考

life-a-tm
人生のイベントや日常生活に密着した比較サイト、情報サイト等様々なウェブサービスを企画・開発・運営
https://life.a-tm.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