などの疑問や悩みを解決してまいります。
JavaScript学習者がぶつかる壁の1つとして非同期処理が当てはまるのではないのでしょうか。
開発においてとても重要な役割を持つ非同期処理ですが、少々ややこしく初学者は敬遠したくなりがちです。
今回は非同期処理を学びたい方、学びたいと考えているが「難しそう」と敬遠している方に向けて説明していきたいと思います。
非同期処理とは
非同期処理とは別に同期処理というものがあるので、先に同期処理から説明します。
同期処理とは、順番に処理を実行していくことを言います。
つまり1つの処理が実行し終えるまでは、次の処理に移行しないということです。
対して非同期処理は、1つの処理が終了するのを待つことなく、別の処理を実行することができます。
例えば煮込み料理を作るとします。
同期処理では煮込んでいる間は何もすることができません。煮込みが完了してからようやく別の作業ができるようになります。
それに対して非同期処理は、煮込んでいる間に料理に使った調理器具を洗ったり、キッチンを掃除することができます。
JavaScriptはシングルスレッドである
JavaScriptは上から順に1つずつ処理を実行していくことしかできません。
つまり複数の処理を同時に行う事ができないのです。
例えば以下のコードを見てください。
console.log("1つ目の処理");
console.log("2つ目の処理");
console.log("3つ目の処理");
console.log("4つ目の処理");
コンソールに値を出力するだけの処理を4つ記述しています。
この実行結果は以下です。
// 実行結果
1つ目の処理
2つ目の処理
3つ目の処理
4つ目の処理
このように上から順に処理が実行されているのが分かるかと思います。
これがシングルスレッドです。
非同期処理をする方法
ここからは非同期処理をする方法について説明していきます。
setTimeout
setTimeout
は、非同期処理を行う事ができるWebAPIのうちTimers APIに分類される一部です。
setTimeout
を使用することで、その処理の開始時間を設定することができます。
実際に使用したコードは以下です。
console.log("1つ目の処理");
console.log("2つ目の処理");
setTimeout(() => console.log("3つ目の処理"), 3000); // 3秒後に処理が実行
console.log("4つ目の処理");
こちらを実行すると以下のような結果になります。
// 実行結果
1つ目の処理
2つ目の処理
4つ目の処理
3つ目の処理
このように1つ目、2つ目は通常通り処理が実行されていますが、次に処理が完了したのは4つ目の処理で、そのあと遅れて3つ目の処理が完了してコンソールに表示されたかと思います。
これはsetTimeout
で処理を3秒後に実行させる指定をしているからです。
そのためsetTimeout
が実行された処理よりも、先に4つ目の処理が実行されたため処理順序が変更になっているということになります。
Promise
Promise
とは、非同期処理を行う事ができるオブジェクトで、非同期処理をより簡単に、そして可読性が上がるように書くことができます。
Promise
では非同期処理の一連の流れを以下のように記述することができます。
new Promise(function(resolve, reject) {
// ここに非同期処理を記述する
}).then(function(result) {
// 成功時の処理を記述する
}).catch(function(error) {
// エラーハンドリングを記述する
}).finally(function() {
// 成功・失敗に関わらず最後に実行する処理を記述する(ES2021から追加されたもの)
});
then
、catch
、finally
といったメソッドがあります。new Promise(function(resolve, reject) { ... })
new Promise(function(resolve, reject) { ... })
の中には記述された非同期処理を実行します。
関数の引数にはresolve
関数と reject
関数が渡されています。
resolve
関数には成功時の値を、reject
関数には失敗時の値を渡します。
.then(function(result) { ... })
.then(function(result) { ... })
の中には、Promise
の処理が成功した場合に実行される内容を記述します。そのため成功時の結果はresolve
関数に渡ります。
.catch(function(error) { ... })
.catch(function(error) { ... })
の中には、Promise
の処理が失敗した場合に実行される内容を記述します。そのため失敗時の結果はreject
関数に渡ります。
.finally(function() { ... })
.finally(function() { ... })
の中には、Promise
の処理の成功・失敗に関わらず最後に実行する処理をします。
使用例
上記の説明を踏まえてPromise
の使用例を紹介します。
以下の例ではランダムな値を生成し、その値が50以上なら「成功」、未満であれば「失敗」とした非同期処理を実行しています。
const getRandomNumberAsync = () => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
const randomNum = Math.floor(Math.random() * 100);
if (randomNum >= 50) {
// ランダムに生成した数値が50以上か、未満かで処理内容を分ける
resolve(randomNum);
} else {
reject(randomNum);
}
}, 3000);
});
};
getRandomNumberAsync()
.then((result) => {
console.log("Success:", result); // 実行結果が「成功」の場合はこちらの処理が実行される
})
.catch((error) => {
console.error("Error:", error); // 実行結果が「失敗」の場合はこちらの処理が実行される
})
.finally(() => {
console.log("非同期処理を終了します。"); // 成功・失敗に関わらず最後に実行される
});
getRandomNumberAsync
関数では、ランダムに生成した数値が50以上であれば「成功」として、未満であれば「失敗」という処理を3秒後に実行させます。
setTimeout
を利用していますが、setTimeout
を利用しなくても、非同期処理の実行が終わるまでは以降の処理は実行されません。50以上の数値であれば成功とみなされresolve
にその値が渡り、then
メソッドが実行されます。
また50未満であれば失敗とみなされreject
に値が渡り、catch
メソッドが実行されます。
resolve
に渡った値をresult
に、reject
に渡った値をerror
に渡しています。最後の処理のfinally
メソッドは成功・失敗に関わらず実行されるのでどちらの結果でも処理が実行されます。
処理結果は以下の通りになるはずです。
// 実行結果
// 成功の場合
Success: 70
非同期処理を終了します。
// 失敗の場合
Error: 8
(anonymous)
Promise.catch (async)
(anonymous)
script.js:22 非同期処理を終了します。
asyncとawait
Promise
をより簡潔に扱うことができるのがこのasync
とawait
です。
async
async
は関数名の前に付けることができます。async
を指定することで非同期関数となりPromise
を返します。
async function getRandomNumberAsync() {}
Promise
であるので、then
、catch
、finally
プロパティが使用できます。await
await
は非同期関数の処理が終了するのを制御することができる記述です。
つまりawait
を記述した処理は、非同期処理が終わるのを待つことができるということです。
await
はasync
関数内で記述しないとエラーになりますのでセットで使用するように覚えておきましょう。
awite getRandomNumberAsync()
使用例
先程のPromise
で使用例を紹介しましたが、そのコードでasync
とawait
を使った書き方に変更しました。
// 非同期処理の内容は変更なし
const getRandomNumberAsync = () => {
return new Promise(function (resolve, reject) {
setTimeout(() => {
const randomNum = Math.floor(Math.random() * 100);
if (randomNum >= 50) {
resolve(randomNum);
} else {
reject(randomNum);
}
}, 3000);
});
};
// 非同期処理の結果の表示方法が変更
async function getRandomNumberAsync() {
try {
const result = await getRandomNumberAsync();
console.log("Success:", result);
} catch (err) {
console.error("Error:", err);
} finally {
console.log("非同期処理を終了します。");
}
}
executeAsync();
then
メソッドではなくて、try
が出てきました…。あと書き方も微妙に違う。まずasync function getRandomNumberAsync() {}
で非同期関数であることを指定しています。
async
を付けることで処理内でawait
を使用することができるようになります。
処理内にはconst result = await getRandomNumberAsync();
とあります。
これはgetRandomNumberAsync
関数の処理が実行され終わるまで待つ形になるので、非同期的に制御ができるようになります。
例外処理
Promise
の解説ではthen
、catch
、finally
メソッドを使用して成功時の処理、失敗時の処理を設定していました。
今回async
とawait
を使用した処理の中では、例外処理と言われる構文を使用しました。
例外処理とはtry…catch…finally
で構成される構文で、try
内の処理で例外が発生した場合、その時点でtry
での処理は中断されcatch
内の処理に移行することができます。
先程紹介したコードを今一度確認してみましょう。
async function getRandomNumberAsync() {
try {
const result = await getRandomNumberAsync();
console.log("Success:", result);
} catch (err) {
console.error("Error:", err);
} finally {
console.log("非同期処理を終了します。");
}
}
executeAsync();
例えばtry
の中ではawait getRandomNumberAsync();
とあります。
これはgetRandomNumberAsync
関数の処理の終わりを待って、以降の処理を実行していくという処理ですが、この関数で何かエラーだったり例外があったとします。
そのときにこの処理の後にconsole.log("Success:", result);
の成功した場合の処理がありますが、この処理を実行する前にcatch
内の処理に移行することができます。
catch
内の処理にはエラー(例外)があった場合の処理を記述します。
finally
の役割は先程説明したthen
、catch
、finally
メソッドのfinally
メソッドと同じ意味でtry
やcatch
の後に必ず実行されるものです。
finally
は省略することが可能です。throw文
意図的にcatch
の処理に移行させることもできます。それがthrow
文です。
throw
文を使用することで、処理が実行された瞬間処理を中断しcatch
の処理に移行します。
async function executeAsync() {
try {
const result = await getRandomNumberAsync();
throw new Error("処理が中断されました。catchに移行します。"); // thorow文
console.log("Success:", result); // この処理は実行されません
} catch (err) {
console.log(err);
} finally {
console.log("非同期処理を終了します。");
}
}
executeAsync();
まとめ
ここまで読んでいただきありがとうございました。
非同期処理のまとめです。
ポイント
- 同期処理は、処理を1つずつ順番に実行していく
- 非同期処理は、1つの処理が終了するのを待つことなく別の処理を実行できる
- JavaScriptはシングルスレッドである
setTimeout
は、処理にタイマーを設定し非同期処理を行うことができるPromise
は、非同期処理を行う事ができるオブジェクトPromise
は、then
、catch
、finally
メソッドを使用して、成功、失敗時の処理を出し分けるasync
を指定することで非同期関数となりPromise
を返すawait
は非同期関数の処理が終了するのを制御することができる- 例外処理とは
try…catch…finally
で構成される構文で、try
内の処理で例外が発生した場合、その時点でtry
での処理は中断されcatch
内の処理に移行する throw
文を使用することで、処理が実行された瞬間処理を中断しcatch
の処理に移行できる