はじめに
この記事では、JavaScript における 非同期処理の内容を、まとめています。
この記事の目的
- JavaScript における 非同期処理 の基本 のおさらい
- 自身の備忘録 兼 アウトプット
この記事の読者の想定
-
JavaScript における 非同期処理の基本をおさらいしたい方 - 非同期処理 をある程度理解してるけど
なんとなくで使用している方 -
コールバック関数・Promise・async/awaitがあるけど、どう違うの?という方
同期処理
非同期処理 の理解を進めるため、まずは 同期処理 を理解します。
同期処理 とは?
同期処理とは、書かれた順番通りに処理が実行される仕組み のことです。
同期処理では、前のタスク が完了してから、次のタスク が実行されます。
(時間軸) →
[タスク1]
[タスク2]
[タスク3]
同期処理 のコード例
console.log('同期処理を開始しました');
const syncTask1 = () => {
console.log("タスク1 完了");
};
const syncTask2 = () => {
console.log("タスク2 完了");
};
const syncTask3 = () => {
console.log("タスク3 完了");
};
// 上から順番に実行
syncTask1();
syncTask2();
syncTask3();
// (結果)
// 同期処理を開始しました
// タスク1 完了
// タスク2 完了
// タスク3 完了
同期処理 のメリット
同期処理 のメリットはもちろんありますが、
コードが読みやすい
コードがシンプルなので、読みやすい
デバッグしやすい
タスク が順番に実行されるので、デバッグしやすい
エラーハンドリングしやすい
前のタスクの完了後 に 次のタスクの実行 となるので、エラー発生時にすぐ検知しやすい
同期処理 のデメリット
以下のようなデメリットがあります。
待機時間の発生
1つのタスク が終わるまで待機してしまうため、複数タスク を行う際に効率が悪い
応答性の低下
タスク が長時間かかる場合、非同期処理 と比べて、ユーザーへの応答性 が低下する
複雑な処理の難易度が高い
複雑なタスク の場合、コードの管理が難しくなる
同期処理 で、実行に時間のかかる処理がプログラムの途中に入ってきた途端、プログラム全体の実行完了に時間がかかるようになってしまいます。
これがアプリ上などで再現されてしまうと、ユーザー体験も損なわれてしまいます。
この対策として、非同期処理 が必要なんですね。
非同期処理
同期処理 が理解できましたので、次に 非同期処理 を理解します。
非同期処理 とは?
非同期処理とは、処理が非順次的に実行される方式 のことです。
非同期処理では、タスクを並行して実行できるため、前のタスクの完了 を待たずに、別のタスク を進行できます。
(時間軸) →
[タスク1]
[タスク2]
[タスク3]
[タスク4]
[タスク5]
非同期処理 のコード例
// 以下は、`setTimeout関数`を利用し、それぞれ異なる時間で完了する 非同期処理です。
console.log('非同期処理を開始しました');
setTimeout(() => {
console.log('タスク1 完了');
}, 2000);
setTimeout(() => {
console.log('タスク2 完了');
}, 1000);
setTimeout(() => {
console.log('タスク3 完了');
}, 3000);
// (結果)
// 非同期処理を開始しました
// タスク2 完了
// タスク1 完了
// タスク3 完了
非同期処理 のメリット
非同期処理では、以下のようなメリットがあります。
タスクの効率が良い
他のタスク をブロックせずに進行できるため、タスクの効率 が良くなる
タスクの待機時間が少ない
タスクの効率 が向上し、待機時間 を最小限に抑えられる
複雑なタスクの管理がしやすい
タスク並行性 の実現により、複雑なタスク を管理しやすくなる
複数のタスク を 最短で実行したい場合を考える
同期処理 で、前のタスク完了後 に 次のタスクを処理する、だと効率悪いですよね。
非同期処理 では、前のタスク完了 を待たずに 次のタスク の実行が可能、なので効率が良いんですね。
JavaScriptの実行 についての事前理解
- JavaScriptの実行 は、
ブラウザ上のメインスレッド(UIスレッド)という所で行われます。 - メインスレッドは、表示の更新など UIに関する処理 も行っています。
同期処理で 非常に重いタスク を実行した場合
- メインスレッド が JSの重い処理 で占有され、
- 表示が更新されずフリーズしたようになってしまい、
- アプリ上などでの快適な操作性(UX)が低下してしまいます。
非同期処理が重要な理由
- 上記のような事態を避ける方法の一つとしては、タスクを別々で実行すれば良く、
その解決策が 非同期処理 というわけですね。 - 非同期処理 は 同期処理 と比較して、プログラムの内容が複雑になりがちですが、
それでも重要とされる大きな理由は、快適な操作性を確保(UX向上)できるためです。
setTimeout / setInterval関数 では、なぜ非同期が実現できている?
setTimeout / setInterval関数は、ブラウザが提供する TimerAPI という 外部API を利用しています。JavaScriptでは、このAPIに処理を任せるので、シングルスレッド でも 非同期処理 が実現できています。
非同期処理 の 実装方法
前置きが長くなりましたが、ここからが本番です。
非同期処理 を JavaScript で行う場合、以下の方法があります。
1つずつ、おさらいしていきたいと思います。
コールバック関数Promiseasync/await
非同期処理① - コールバック関数
コールバック関数 とは?
コールバック関数とは、ある関数の引数として渡される関数のことです。
ある関数が一定の処理を終えたら呼び出されるため、コールバック関数と呼ばれます。
コールバック関数は、非同期処理のもっとも基本的な手法です。
代表的な非同期処理のコールバック関数
setTimeout関数
- setTimeout関数 は、
指定した時間が経過した後にコールバック関数を実行します。
// この例では、sayHello関数 が コールバック関数 です。
// setTimeout によって 指定された時間(3秒) が経過した後に、実行されます。
const sayHello = () => {
console.log("Hello, world!");
}
console.log("3秒後にメッセージを表示します...");
setTimeout(sayHello, 3000); // 3秒後に sayHello関数 を実行します。
// 3秒後にメッセージを表示します...
// Hello, world!
setInterval関数
- setInterval は、
指定した間隔で繰り返しコールバック関数を実行します。
// この例では、repeatMessage関数 が コールバック関数です。
// setInterval によって 指定された間隔(1秒ごと) で、繰り返し実行されます。
// また、setTimeout内の 無名関数() も コールバック関数 です。
// これは 5秒後 に実行されて setInterval を停止します。
const repeatMessage = () => {
console.log("このメッセージは1秒ごとに表示されます");
}
console.log("メッセージの表示を開始します...");
// 1秒ごとに `repeatMessage` 関数を実行
const intervalId = setInterval(repeatMessage, 1000);
// 5秒後に `setInterval` を停止
setTimeout(() => {
clearInterval(intervalId); // clearInterval で 実行を停止します。
console.log("メッセージの表示を停止しました");
}, 5000);
コールバック関数を使えば 非同期処理が可能なので、めでたし、となりそうな所ですが、
問題があります。非同期処理を順々に実行するのはコードが複雑になりがちです。。
コールバック地獄 🟥 を作る
- 非同期処理を理解するためには、
コールバック地獄を作ってみると理解がしやすい、とよく聞くのでやってみます。 - JavaScript でコールバック地獄を再現するためには、
非同期のコード をひたすらネストした構造で書くと作ることができます。以下は、簡単な例です。
// コールバック地獄 の例
// タスク1
const firstTask = (callback) => {
setTimeout(() => {
console.log("タスク1 完了");
callback();
}, 1000);
}
// タスク2
const secondTask = (callback) => {
setTimeout(() => {
console.log("タスク2 完了");
callback();
}, 1000);
}
// タスク3
const thirdTask = (callback) => {
setTimeout(() => {
console.log("タスク3 完了");
callback();
}, 1000);
}
// 実行
// ネストが深く、可読性が悪い。。
firstTask(() => {
secondTask(() => {
thirdTask(() => {
console.log("全タスク 完了");
});
});
});
この例 は 浅い地獄ですが、firstTask の 完了後に secondTask が実行され、その後に thirdTask が実行されるような、非同期のネスト構造 となっています。
このようにネストが深くなると応答性・コードの可読性が低下します。
ES6以前のJavaScript では、非同期処理のためにはコールバック関数を使う方法しかありませんでした。
そこで、この問題を解決するために、Promise や async/await ができたんですね。
非同期処理② - Promiss
Promiss とは?
Promise(プロミス)とは、非同期処理をより効率的に扱うためのJavaScriptの機能です。
Promiseは、非同期操作が成功または失敗したかを表現し、その結果を返すオブジェクトです。
主な特徴は以下です。
1. 非同期操作の状態を表現
Promiseは、非同期操作の状態 を 3つの状態で表現します。
-
Pending(保留中): 初期状態で、操作の完了または失敗を待っています。 -
Fulfilled(完了): 操作が成功した状態です。 -
Rejected(失敗): 操作が失敗した状態です。
2. チェーン可能な構造
Promiseは、.then()メソッドを使用して、非同期操作が完了した後に実行するコールバック関数 を指定できます。これにより、複数の非同期操作をチェーンのように連鎖させて実行することができます。
3. エラーハンドリング
Promiseは、.catch()メソッドを使用して、非同期操作の失敗をキャッチし、適切に処理することができます。
Promiseの基本的な流れ
-
new Promise()によって、Promiseオブジェクト ができます。 - 最初は
待機の状態。 -
resolveで完了の状態。 - 次の処理に行ける。
Promiseの基本的な例
以下の例では、asyncFunction という非同期処理を含む関数 が Promise を返します。
非同期処理が成功の場合はresolve()を呼び出し、失敗の場合はreject()を呼び出す。
.then()メソッド で 成功時の処理を、.catch()メソッド で 失敗時の処理を定義します。
// 非同期処理を含む関数
const asyncFunction = () => {
return new Promise((resolve, reject) => {
// 非同期操作
setTimeout(() => {
const success = true;
if (success) {
resolve("成功"); // 操作が成功した場合、解決された状態にする
} else {
reject(new Error("失敗")); // 操作が失敗した場合、拒否された状態にする
}
}, 2000);
});
}
// 非同期処理の実行と結果の処理
asyncFunction()
.then(result => {
console.log("成功:", result);
})
.catch(error => {
console.error("エラー:", error);
});
- 以下は、Promise の
.then()を使用してコールバック地獄 を解消した例です。
// タスク1
const firstTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク1 完了");
resolve();
}, 1000);
});
}
// タスク2
const secondTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク2 完了");
resolve();
}, 1000);
});
}
// タスク3
const thirdTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク3 完了");
resolve();
}, 1000);
});
}
// 実行
firstTask()
.then(() => secondTask())
.then(() => thirdTask())
.then(() => {
console.log("全タスク 完了");
});
- 先ほどの コールバック地獄の記述 に戻って、改善具合を確認してみてください。
- Promiseを使用することで、直列的な処理の流れを表現することができ、コールバック地獄を回避し、
より読みやすく保守しやすい非同期コードを書くことができ、マシになりました。と言いたい所ですが、これでも冗長に見えますね。。
そこで、async/awaitの出番です。
非同期処理③ - async/await
async/await とは?
async/awaitは、非同期処理をより直感的かつ同期的に書けるようにするための機能です。
JavaScript の async関数 と await式 を組み合わせて使用します。
async
非同期処理を行う関数の宣言時に使用します。
asyncキーワードを関数の前に付けることで、その関数が非同期であることを示します。
await
非同期処理の結果を待ち受ける式の前に置きます。
await式は、Promiseを返す非同期関数の呼び出しや、Promiseを返す式の前に置かれることで、そのPromiseの解決を待ちます。
基本的な考え方
- 非同期処理をわかりやすく書ける救世主
- Promise のメソッドチェーンもなんとなく見辛い。
- async/await を使って、
普通に書けるようにしよう。
- async/await を使うことで、非同期処理を書く際にコールバック関数やPromiseチェーンをネストする必要がなくなり、コードがシンプルで読みやすくなります。
以下は、async/await を使用した非同期処理の例です
// 非同期処理を含む関数
// タスク1
const firstTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク1 完了");
resolve();
}, 1000);
});
}
// タスク2
const secondTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク2 完了");
resolve();
}, 1000);
});
}
// タスク3
const thirdTask = () => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("タスク3 完了");
resolve();
}, 1000);
});
}
// async/await で 非同期処理 を記述
const executeTasks = async () => {
await firstTask();
await secondTask();
await thirdTask();
console.log("全タスク 完了");
}
// 実行
executeTasks();
コールバック関数 や Promise と比較すると、async/await が 一番わかりやすいですね!
まとめ
非同期処理の歴史の流れを振り返ると、
-
非同期処理が誕生- ただし コールバック地獄 が辛い
-
Promiseの誕生- ただし .then が見辛い
-
async/awaitの誕生- 普通に書けるようになった!
いかがだったでしょうか?
JavaScriptにおける非同期処理について、基本的な情報が身になったと感じました。
読んでいただいた方にとって、JSの非同期の理解 の一助になっていれば幸いです。
参考