初学者の方の難敵の一つに「非同期処理」というものがあります。
そもそも考え方が難しいですし、学習教材やスクールでは同期処理をメインに学ぶからですね。
とはいえ近年のサイトやアプリでは非同期処理やそれに近い処理は当たり前のように要求されるので、どこかで必ず理解しておく必要があるのも事実です。
ちなみにJavaScriptにはsetTimeoutとsetIntervalという非同期処理を手軽に理解するのに打ってつけの関数が用意されていて、自分もこちらから始めたことで非同期処理がスムーズに頭に入った経験がありましたので紹介して行こうと思います。
また動画もあるので必要に応じて活用してください。
JavaScriptにおけるsetTimeoutの使い方
まずsetTimeoutという関数を紹介します。
こちらの関数は簡単に言うと「〇〇だけ時間が経過したら▲▲の処理を実行する」と言うものです。
使い方は以下のような形です。
setTimeout(test, 3000);
function test() {
console.log("test");
}
// 3秒後に「test」と出力される
setTimeoutは引数を二つ取り、第一引数には「時間経過後に実行したい処理」、第二引数には「経過させたい時間」です。
ちなみに第二引数の時間については「1秒=1,000」といった換算を行うルールになっていて、上記のコードの場合だと3000なので3秒という形です。
日常生活では使わない換算ですが、プログラムの世界のルールになっています。
上記のコードに別の処理を追加してみたいと思います。
setTimeout(test, 3000);
function test() {
console.log("test");
}
// ここを追加
console.log("hello");
// 「hello」と出力されて、3秒後に「test」と出力される
console.log(“hello”)を下の行に追加しました。
プログラムの一般的な考えとしては「上から順番に実行する」といった具合なんですが、最初に書いたsetTimeoutの動きの影響でコンソールログに出力される順番が逆になります。
あえて順番通りに実行させない動作を作る、ザックリですがこれが非同期処理の考え方になります。
コールバック関数の書き方
setTimeoutのように「何かを待ってから実行する関数」のことをコールバック関数と呼んだりします。
コールバック関数というと慣れない言葉ですが、普通の関数と同じだと思って大丈夫です。
ただJavaScriptの場合、関数の書き方にパターンがあるようにコールバック関数にも書き方に種類があります。
// ①通常関数の書き方をするコールバック関数
setTimeout(test, 3000);
function test() {
console.log("test");
}
// ②無名関数の書き方をするコールバック関数
setTimeout(function () {
console.log("test");
}, 3000);
// ③アロー関数の書き方をするコールバック関数
setTimeout(() => {
console.log("test");
}, 3000);
JavaScriptの関数の書き方に自信がない方は、過去に詳しく解説しているので良ければ確認して見てください。
setTimeoutをキャンセルするclearTimeoutの使い方
setTimeoutは自分で決めた時間が経過したら処理を実行するのですが、時間が経っても処理を実行させない関数もJavaScriptには用意されています。
const result = setTimeout(test, 3000);
function test() {
console.log("test");
}
clearTimeout(result);
// 3秒経っても何も出力されない
clearTimeoutという関数を使うと、setTimeoutで作ったコールバック関数を止めることができます。
使い方はclearTimeoutの引数にsetTimeoutのIDを指定します。
「setTimeoutのID」とは何のことでしょうか?
上記のコードの1行目に注目すると、setTimeoutを定数resultに格納しているのが分かります。
試しにresultをコンソールで見てみます。
const result = setTimeout(test, 3000);
function test() {
console.log("test");
}
// ここを追加
console.log(result); // 1と出力される
〜省略〜
コンソールには数字の「1」が出力されました。
こちらがsetTimeoutのIDになります、言葉通り特定するための連番のようなものです。
上記のコードではシンプルな作りになっていますが、もしもsetTimeoutが10個書かれていたらclearTimeoutから見た時に「どのsetTimeoutを止めるんだ?」となるからです。
JavaScriptにおけるsetIntervalの使い方
続いてsetIntervalについて紹介します。
こちら簡単に言うと「〇〇の間隔で▲▲を繰り返し実行する」というものです。
setInterval(test, 1000);
function test() {
console.count("test");
}
// 1秒ごとに「test」が出力され続ける
setIntervalも二つの引数を取って、第一引数に「繰り返したい処理」、第二引数に「繰り返す間隔」となります。
第二引数の時間はsetTimeout同様に「1秒=1,000」の換算です。
上記のコードでは「test」と言う文字が1秒ごとにコンソールに出力され続けます。
こちらもコールバック関数の書き方は以下のように3パターンの書き方になるでしょう。
// ①通常関数の書き方をするコールバック関数
setInterval(test, 1000);
function test() {
console.count("test");
}
// ②無名関数の書き方をするコールバック関数
setInterval(function () {
console.count("test");
}, 1000);
// ③アロー関数の書き方をするコールバック関数
setInterval(() => {
console.count("test");
}, 1000);
setIntervalをキャンセルするclearIntervalの使い方
setIntervalは一度実行し始めるとコールバック関数が永遠に動き続けます。
JavaScriptではclearIntervalという関数を使うとsetIntervalの動きを止めることができます。
const result = setInterval(test, 1000);
function test() {
console.count("test");
}
clearInterval(result);
// 1秒経っても「test」と出力されない
setTimeout同様にsetIntervalも定数に格納することで、定数が「setIntervalのID」として使うことができます。
clearIntervalの引数にsetIntervalのIDを入れることで、setIntervalの動作を止めてくれます。
上記のコードだと開始して止めているのですが、「10回繰り返してから止める」なんてことも条件分岐などと組み合わせて実現できますね。
setTimeoutとsetIntervalを紹介してプログラムの少し変わった動作を体験してもらえたんじゃないでしょうか?
JavaScriptにおける非同期処理の基本
最後にJavaScriptにおける非同期処理の書き方を紹介します。
JavaScriptの場合、setTimeoutやsetIntervalのような特定のメソッドを使わなくても非同期処理を作ることができます。
Promiseという名前の組み込みオブジェクトがありインスタンス化するだけです。
const promise = new Promise((resolve, reject) => {
// ここに処理内容を書く
});
またPromiseではresolveとrejectという2つの引数を取るのですが、「resolve = 成功」「reject = 失敗」という風に覚えてもらえれば大丈夫です。
例えば以下のような使い方になります。
const num = 10;
const promise = new Promise((resolve, reject) => {
if (num > 0) {
resolve("OKです");
} else {
reject("エラーです");
}
});
定数numという数字の大きさによって成功と失敗を分ける条件分岐を作りました。
resolveとrejectはともにメソッドになっていて「( )」の中で定義した値を次の非同期処理に渡します。
const num = 10;
const promise = new Promise((resolve, reject) => {
if (num > 0) {
resolve("OKです");
} else {
reject("エラーです");
}
});
promise
.then((result) => {
console.log(result); // 成功した場合は「OKです」と表示される
})
.catch((result) => {
console.log(result); // 失敗した場合は「エラーです」と表示される
});
resolveを実行した場合(成功した場合)はthenメソッドに移動して次の非同期処理になります。
上記のコードではthenの引数resultには「”OKです”」が渡されています。
一方でrejectを実行した場合(失敗した場合)はcatchメソッドに移動してエラー処理をして終わります。
catchの引数resultには「”エラーです”」が渡されています。
catchメソッドはエラー処理なのでプログラム自体が終わるわけですが、成功した場合のthenメソッドは別のthenメソッドを繋げることができます。
const num = 10;
const promise = new Promise((resolve, reject) => {
if (num > 0) {
resolve("OKです");
} else {
reject("エラーです");
}
});
promise
.then((result) => {
console.log(result); // 成功した場合は「OKです」と表示される
})
.then(() => { // ここを追加
console.log("完了です"); // 「OKです」と表示された後に「完了です」と表示される
})
.catch((result) => {
console.log(result); // 失敗した場合は「エラーです」と表示される
});
2つ以上のthenメソッドを同時に実行することはありません。
setInervalやsetTimeoutの例でもお話したように非同期処理は1個ずつの処理を順番に実行していくので、thenメソッドが何個もつながっている時は上から順番に実行していきます。
ちなみに成功しても失敗しても実行したい処理がある場合はfinallyメソッドというものが用意されています。
const num = 10;
const promise = new Promise((resolve, reject) => {
if (num > 0) {
resolve("OKです");
} else {
reject("エラーです");
}
});
promise
.then((result) => {
console.log(result); // 成功した場合は「OKです」と表示される
})
.then(() => { // ここを追加
console.log("完了です"); // 「OKです」と表示された後に「完了です」と表示される
})
.catch((result) => {
console.log(result); // 失敗した場合は「エラーです」と表示される
})
.finally(() => { // ここを追加
console.log("共通の処理です"); // 前処理がどうなろうと必ず最後に「共通の処理です」と表示される
});
finallyを使うパターンは例えばデータベースを接続する処理です。
PHPなどのバックエンド言語を触った経験のある方なら分かると思いますが、データベースは接続と切断を必ず行います。
実行したい内容が実行されてもエラーになったとしても、どちらにしてもデータベースに接続して最後に切断して終わることに変わりありませんよね。
そういった共通処理にfinallyメソッドは使います。
Promise、then、catchなど何やら専門用語がたくさん登場しましたが、結局は非同期処理が「成功したか」「失敗したか」で分けているだけです。
ちなみにですがPromiseなどの専門用語を使わなくても基本的なJavaScriptの文法だけで同じことは実現できます。
Promiseを使わずに非同期処理を書くのであれば以下のような形です。
function1(() => {
function2(() => {
if (!data) {
console.log("エラーです");
} else {
console.log("OKです");
}
});
});
初学者の方でも馴染みのある言葉しかないですね。
しかし実際には上記のようなコードはとても使いづらいです。
見た目でも分かるように何段階にも入れ子になっており、とても分かりづらいですし間違えやすいです。
非同期処理というものは、resolveとrejectで成功と失敗を判定してそれぞれの結果でthenメソッドかcatchメソッドに移るという、特殊な動きをするので基本的な文法だけでは限界があるのです。
一方で「そもそも何で成功と失敗を分けるの?」と思われた方がいるかもしれません。
非同期処理は多くの場合、APIやデータベースを使った処理になります。
つまりサーバーが絡むプログラムを書くわけです。
イメージして欲しいのですが、サーバーは私たちが開発、もしくはユーザーが実行する場所とは違う場所に設置されていますよね。
ご自宅にサーバーがある方はほとんどいないと思います。
サーバーはプログラムの内容とは別の理由でも不具合を起こすことがあります。
「アクセス過多」「ハッキング」「システム障害」などニュースで一度は聞いたことがあるはずです。
そういったトラブルに私たち開発者やユーザーは直接対処することができません。
どれだけ正しいプログラムを書いたとしても100%の動作保証ができないのです。
そのため事前に成功と失敗の2パターンを想定してプログラムを書くことになったわけです。
いろんな専門用語が登場して苦手意識を持ちがちな非同期処理ですが、言葉が慣れないだけで理屈としてはイメージできるものがほとんどです。
まずは前段で紹介したsetTimeoutとsetIntervalを何回か練習してみて「考え方」に慣れるところから始めてみると良いでしょう。