JavaScriptを使う上で、
非同期処理の知識は、やはり避けては通れないでしょう。
それぐらい特にJavaScriptにおいては、
非同期処理を使う機会が多い印象です。
本記事ではそんなJavaScriptの非同期処理について
触れていきたいと思います。
JavaScriptは、非同期処理をサポートするために設計されたシングルスレッドのプログラミング言語です。非同期処理は、時間のかかる操作(ネットワーク通信やファイルの読み書きなど)を他の処理をブロックせずに実行するために欠かせません。
この記事では、JavaScriptの非同期処理について、基本概念から具体例、そして効果的な実装方法までをわかりやすく解説します。
ちなみに、JavaScript自体がまだよくわからんという人は、
下記の記事で紹介していますので、先にこちらを御覧ください
https://reisuta.com/js_overview_variable/
非同期処理とは
そもそも非同期処理とはなんでしょう。
MDNの解説をみてみると、
非同期プログラミングは、長く続く可能性のあるタスクを開始しても、そのタスクが完了するまで待つのではなく、そのタスクの実行中も他のイベントに応答できるようにする技術です。タスクが完了すると、プログラムはその結果を表示します。
MDNより引用 https://developer.mozilla.org/ja/docs/Learn/JavaScript/Asynchronous/Introducing
みたいな感じで書かれていました。
要するに、ある処理が完了するまで待つのではなく、別の処理を並行して実行できる仕組みと
言えるでしょう。
例えばapiを叩く場合ときなどは、
それなりに時間がかかったりするので、
通常非同期で実装する必要があると思います。
ちなみに、非同期処理自体は
JavaScript固有のものではなく、
他のプログラミング言語とかにも存在する概念です。
ただ、最も使用頻度が多かったり、非同期処理で有名な言語はなんだって言われると、
JavaScriptを答える人が多い気はしています。
余談ですが、例えばNeovimとかでファイルを保存した際に、
Lintを走らせたりするのも非同期処理の活用例かもしれないですね。
同期処理と非同期処理の違い
非同期処理というぐらいなので、
同期処理というのもあります。
同期処理は、一つの処理が終わるまで次の処理は実行されない、
平たく言うといつも書いているような上から順番に逐次的に実行されるような処理です。
// 同期処理の例
console.log("処理1開始");
console.log("処理1終了");
console.log("処理2開始");
console.log("処理2終了");
// 出力
// 処理1開始
// 処理1終了
// 処理2開始
// 処理2終了
なんのひねりもなく、
上から順番に表示されると思います。
これは、console.log("処理1開始")
が終わったら、
次の行を実行するというような感じで、
前の処理が終わらないと次に進まない形式になっていると思います。
一方で非同期処理の場合は、
前の処理が終わるのとは関係なく、
次の処理に反応できるようなプログラミング手法でしょう。
//非同期処理の例
console.log("処理1開始");
setTimeout(() => {
console.log("非同期処理終了");
}, 2000);
console.log("処理2開始");
// 出力
// 処理1開始
// 処理2開始
// 非同期処理終了
同期処理的にみると、真ん中のsetTimeoutが1つ目の次に実行されるのではと
思いますが、非同期処理の場合、この2つ目の実行が終わるのを待たずに、console.log("処理2開始")
が実行されています。
すなわち、同期処理と非同期処理の違いは、
このような前の処理が終わるのを待ってから次の処理に入るか入らないかの
違いと考えるとわかりやすいかもしれません。
JavaScriptで非同期処理を扱う方法
さて、そんな非同期処理ですが、
メリットはやはり前の処理の結果を待たなくていいので、
同期処理よりもパフォーマンスを上げやすいという特徴があります。
そんな非同期処理をJavaScriptでは具体的にどういうコードを使って
書くのか見ていきたいと思います。
setTimeoutとコールバック関数
非同期処理のチュートリアルでだいたい最初に出てくる、
setTimeoutから触れていこうと思います。
また、非同期処理を実装するうえで、
コールバック関数という概念を知っておいたほうが
良いと思うので、こちらも先に触れます。
コールバック関数は、非同期処理が完了したときに呼び出される関数と
言及されたり説明されたりすることが多いかと思います。
この説明だと、微妙に足りない気がしていて、
下記のMDNのリンクによるところの、非同期コールバックの説明になっていると思います。
https://developer.mozilla.org/ja/docs/Glossary/Callback_function
コールバック関数は、本来非同期処理固有のものというわけではなく、
引数として他の関数に渡される関数のことです。(それこそ同期コールバックの例なmapとか)
MDN的に言うなら、この関数が非同期的に呼び出されるか、
同期的に呼び出されるかで、実行結果に影響したりという感じだと思います。
下記はsetTimeout(非同期コールバックの例)と併用した、コールバック関数の例です。
function doSomething(callback) {
console.log("処理を開始します");
setTimeout(() => {
console.log("処理が完了しました");
callback(); // コールバック関数を呼び出す
}, 2000);
}
doSomething(() => {
console.log("次の処理を実行します");
});
ただ、JavaScriptの場合、
コールバック地獄という有名な現象があり、
このイメージからコールバック関数というと非同期処理でしょ!
みたいなイメージになっているのかもしれません。
下記は、そんなコールバック地獄のコード例です。
ネストが激しくて読みづらいですね。
setTimeout(() => {
console.log("1秒後");
setTimeout(() => {
console.log("さらに1秒後");
setTimeout(() => {
console.log("さらにさらに1秒後");
}, 1000);
}, 1000);
}, 1000);
次に触れますが、こんなコールバック地獄を
解消したものが、Promiseやthenになります。
Promise
Promiseは、めちゃくちゃ詳細にかっちり説明すると、
わけがわからなくなってくるぐらい複雑な概念(複雑というより細かい?)ですが、
めちゃくちゃ平たく言うと、非同期処理をより使いやすくするための、
ツールと考えればシンプルだと思います。
というのも、Promiseを使わないで、
非同期コールバック地獄のコードを書いてしまうと、
エラーハンドリングが困難になる点と、そもそもネストになるので、
読みにくくなります。
その点Promiseを使えば、非同期処理の状態(Pending, Fulfilled, Rejected)をハンドリングできるので、
簡潔に実装できるというメリットがあります。
const promise = new Promise((resolve, reject) => {
// 非同期処理
setTimeout(() => {
resolve("成功しました");
}, 2000);
});
promise
.then((result) => {
console.log(result); // 成功時の処理
})
.catch((error) => {
console.error(error); // エラー時の処理
});
それこそコールバック地獄になると、
thenやcatchとかが使えず、
ifとかで各コールバックのエラーを仕分けしなきゃならないので、
文字通り地獄でしょう。
ちなみにPromiseの概念ですが、MDNでは、
Promise オブジェクトは、非同期処理の完了(もしくは失敗)の結果およびその結果の値を表します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
ていう感じで説明されています。
async/await
さて、そんな便利なPromiseですが、
実はもっとモダンな構文があります。
それがasync/await
です
async/awaitを使えば、Promiseチェーンとかをもっとすっきり書くことができます。
const sample = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function call() {
console.log('calling');
const result = await sample();
console.log(result);
// Expected output: "resolved"
}
call()
awaitを使えば、上記のコードの場合、
sample関数が完了するまで待機します。
なので、実行してみると、
resolvedという出力が最後にでることが確認できます。
下記のコードみたいに、順番に実行することもできます。
例えば、apiの呼び出しとかを前のapiの呼び出しが終わってからにしたい
みたいなときにも重宝するでしょう。
awaitを使う場合は、asyncキーワードも通常合わせて使用する必要があります。
async function sequentialTasks() {
const result1 = await new Promise((resolve) =>
setTimeout(() => resolve("1つ目の処理完了"), 1000)
);
console.log(result1);
const result2 = await new Promise((resolve) =>
setTimeout(() => resolve("2つ目の処理完了"), 1000)
);
console.log(result2);
}
sequentialTasks();
ちなみに、Promise.all
を使うと、複数の非同期処理を並列に実行できるので、awaitでいちいち待つのではなく、並行してapiを叩きたいときとかは、
こちらが使えるでしょう。
async function parallelTasks() {
const results = await Promise.all([
new Promise((resolve) => setTimeout(() => resolve("処理1完了"), 1000)),
new Promise((resolve) => setTimeout(() => resolve("処理2完了"), 500)),
]);
console.log(results); // ["処理1完了", "処理2完了"]
}
parallelTasks();
JavaScriptをもっと極めるには
JavaScriptは、フロントエンドを実装するうえで
もはや必須の言語といえますし、
最初にプログラミング言語を学ぶとしたら、
もっとも無難な選択肢といえるぐらい、
良くも悪くも潰しが利くといえる言語でしょう。
そういうこともあり、
みんななんとなくはJavaScriptを知ってはいるけど、
そもそもの言語の歴史とか、
他の言語と比べた特徴とかは意外と知らない人も多いのではないでしょうか。
そこで、そんなJavaScriptをもっと深めるためにおすすめの本を
下記にリンクを張っておきます
よろしければご参考ください。