JavaScript の非同期において「並列」という不適切な単語を使用していたため勉強し直し & 書き直しました。
@shiracamus さんご指摘いただきありがとうございます。
JavaScript の非同期処理はこれが分かればレベルアップできるというか、難しいポイントだなと思います。
今回は「JavaScript中級者から上級者になる」という本アドベントカレンダーの目標に則り、逃げずにしっかり非同期処理について調べていこうと思います。
▼ async, await 編はこちらです
非同期処理とは
非同期処理について知るためにはまず同期処理のイメージを掴む必要があります。
ざっくり説明すると、同期処理ではコードを上から順番に実行します。
(ここでは一旦巻き上げのことは考えないようにしましょう)
const myName = "kotaro";
function greet(name) {
return `Hello ${name}!`;
}
greet(myName);
このような処理は同期処理です。
上から順番に実行されないと、 greet(myName)
のときに myName
がないというエラーが発生するかもしれません。
しかし非同期処理ではある処理と他の処理がうまいこと入れ替わり立ち替わり処理されます。 処理を切り替えながら実行することを並行処理と言います。
JavaScript はシングルスレッド(同時処理むり)の言語なので何かと何かを同時に処理 = 並列処理しているわけではないのですが、上から一方通行の処理ではなく一定の規則で処理順番が変わっている並行処理が JavaScript の非同期です。
非同期処理のメリット
なぜ順番に処理をせず、わざわざ分かりにくい非同期処理を使うのでしょうか。
理由は簡単で、重い処理を行うときに他の処理を妨げないためです。
例えば外部のサイトからデータを引っ張ってきてWebサイトに表示したいとしましょう。
このとき、「外部のサイトからデータを引っ張ってきて」という部分が時間のかかる処理です。
重たい処理を同期的に実施してしまうとその間Webサイトの動作が止まってしまいます。
データを引っ張ってくる部分とは関係ない要素を表示することもできませんし、ユーザーのクリックに反応することもできません。これでは不便ですね。
非同期処理は内部的に処理を切り替えたり、邪魔にならない別の場所で処理したりして、重たい処理が全体の処理を妨げないようにしています。
非同期処理を使う方法1: setTimeout
関数
非同期処理の一番分かりやすい方法を見てみましょう。 setTimeout
です。
function return1() {
console.log(1);
}
function return2() {
console.log(2);
}
function return3() {
console.log(3);
}
return1();
setTimeout(return2, 1000);
return3();
/*
-> 1
-> 3
-> 2
*/
第一引数に実行したい関数、第二引数に遅延させる時間を入れることで関数の実行を遅延させることができます。
setTimeout
は何らかの操作が行われたら◯秒後に関数を実行させたり、画面を数秒後に強制的に遷移させたり、画像をスライドショーのように切り替えたりといった場面で使えます。
非同期処理を使う方法2: Promise
オブジェクト
非同期処理といえば Promise ですね。
PromiseはES2015で導入された非同期処理の状態や結果を表現するビルトインオブジェクトです。 非同期処理はPromiseのインスタンスを返し、そのPromiseインスタンスには状態変化をした際に呼び出されるコールバック関数を登録できます。
非同期処理:Promise/Async Function · JavaScript Primer #jsprimer
▼ オブジェクトやインスタンスについてはこちらの記事で解説しています。
Promise は非同期処理を行なっている関数から返され、いま処理がどのような状態なのかを教えてくれます。また処理が成功した場合と失敗したときの動作を設定できます。例: 成功した場合は値を表示し、失敗したときにはエラーメッセージを出す
Promise には3つの状態があります。
- Pending: 処理中
- FulFilled: 処理成功
- Rejected: 処理失敗
では以下のコードをコンソールなどで実行してみてください。
const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
console.log(fetchPromise);
fetchPromise.then((response) => {
console.log(`レスポンスを受信: ${response.status}`);
console.log(fetchPromise);
});
console.log("リクエストを開始…");
// 参考: https://developer.mozilla.org/ja/docs/Learn/JavaScript/Asynchronous/Promises
こんなふうに表示されたでしょうか?
コードを順番に解析していきましょう。
まず1行目で fetchPromise
を宣言しています。
中身はデータを取得してくる非同期関数である fetch
関数の実行結果、つまり Proise です。
次に2行目で fetchPromise
をコンソールに表示していますね。この時点で表示されたのが Promise {<pending>}
です。まだ処理中ということですね。
次に fetchPromise
を Promise のネイティブメソッドである then
で処理しています。 then
メソッドは「Promise を処理するために status が Pending じゃなくなったら次はこんな動作をしてね」という関数を中に入れることができます。ただし、この部分は Promise を扱っている非同期処理です。
次に他の処理が先に行われるため、「リクエストを開始…」がコンソールに出力されています。
そして最後に then
の中身が実行されました。今回は fetchPromise
が正常に処理されため、200 = OK のステータスコードが返ってきています。Promise の状態は Promise {<fulfilled>: Response}
となっていますね。
Promise の処理の流れは以上のような感じです。
非同期処理を使う方法3: async
await
構文
非同期処理はやはりクセがあり、慣れていないと思いがけない順番で実行されてしまうことが非常に多いです。
そんな非同期処理を同期処理のように書けるのが async
await
構文です。
async
await
については以下の記事で解説しています。
まとめ
- 同期処理ではコードを上から順番に実行する
- JavaScript の非同期処理は並行処理であり、ある処理と他の処理がうまく切り替えられて処理される
- 非同期処理には重い処理を行うときに他の処理を妨げないメリットがある
-
setTimeout
関数は第一引数に実行したい関数、第二引数に遅延させる時間を入れることで関数の実行を遅延させることができる
参考資料
- 非同期処理:Promise/Async Function · JavaScript Primer #jsprimer
- 非同期 JavaScript 入門 - ウェブ開発を学ぶ | MDN
- プロミスの使い方 - ウェブ開発を学ぶ | MDN
- setTimeout() - Web API | MDN
▼ async, await 編はこちらです