はじめに
javascriptやreact、jest、testing-libraryに触れてきてエラーに遭遇したとき、非同期処理が理解できていないから解決できないのではと思いました。
なんとなくわかっているつもりになっている非同期処理について、学んだことをまとめてみました。
同期処理と非同期処理の違い
「非同期処理」という言葉があるということは、同期処理もあるはずです。
同期処理
一つの処理が完了するのを待ってから、次の処理へ進むことと理解しました。
※MDNから引用
const name = "Miriam";
const greeting = `こんにちは。私は ${name} です。`;
console.log(greeting);
// "こんにちは。私は Miriam です。"
コードは上から順番に処理が終わるのを待ってから、次の処理が実行されます。
同期処理のデメリット
一つの処理が完了してから次の処理が実行されるため、処理負荷が大きいと時間がかかってしまいます。
簡易的に処理負荷が大きいものを作ってみました。
メッセージ表示ボタン、テキスト入力欄、表示ボタンを用意しました。
この例では、表示ボタンを押したときに重い処理(10万回ループ処理・コンソールに1を表示する)を実行するため、その間に他の操作ができなくなります。
時間のかかる処理があると他の操作ができなくなることは、同期処理の大きなデメリットです。
import { useState } from 'react';
import './App.css';
function App() {
const [message, setMessage] = useState('');
const onClickDisplay = () => {
const array = [...Array(100_000)];
array.forEach(() => console.log(1));
alert('値入れたよ');
};
const onClickDisplayM = () => {
message === '' ? setMessage('メッセージだよ') : setMessage('');
};
return (
<>
<div>
<h1>同期処理</h1>
<button onClick={() => onClickDisplay()}>表示</button>
<button onClick={() => onClickDisplayM()}>メッセージ表示</button>
<p>{message}</p>
<input type="text" placeholder="なにか入力できるよ" />
</div>
</>
);
}
export default App;
非同期処理
同期処理における「処理が終わるまで待つ」問題を解決するために使われる、
非同期処理の本質は「ある処理の完了を待たずに次の処理に進むこと」と理解しました。
JavaScript の非同期処理の方法
1. コールバック関数とsetTimeout
最も基本的な非同期処理
関数を引数に渡すことができ、引数になっている関数のことをコールバック関数と呼ぶそうです。
function greet(name, callback) {
// setTimeoutで1秒遅らせてコールバックを実行
setTimeout(() => {
callback();
}, 1000);
console.log(`こんにちは、${name}さん!`);
}
console.log("処理を開始します");
greet("A", function() {
console.log("挨拶が終わりました");
});
console.log("次の処理に進みます");
// 出力:
// 処理を開始します
// こんにちは、Aさん!
// 次の処理に進みます
// (1秒後)
// 挨拶が終わりました
setTimeout
を使用しない場合、同期的に処理されるため、コールバック関数が先に実行されてしまいます。
setTimeout
で1秒遅らせることで、処理の順序を制御できます。
⇒「ある処理の完了を待たずに次の処理に進む」が実現できていることを理解しました。
コールバック関数でのデメリット(コールバック地獄)
複数の人に対して順番に挨拶したいとき、以下のコードで書いてみました。
function greet(name, callback) {
// setTimeoutで1秒遅らせてcallbackを実行する
setTimeout(() => callback(), 1000);
console.log(`こんにちは、${name}さん!`);
}
greet("A", function() {
console.log("挨拶が終わりました");
});
greet("B", function() {
console.log("挨拶が終わりました");
});
greet("C", function() {
console.log("挨拶が終わりました");
});
greet("D", function() {
console.log("挨拶が終わりました");
});
// 出力予想:
// こんにちは、Aさん!
// 挨拶が終わりました
// こんにちは、Bさん!
// 挨拶が終わりました
// こんにちは、Cさん!
// 挨拶が終わりました
// こんにちは、Dさん!
// 挨拶が終わりました
結果は以下の通りになります。
こんにちは、Aさん!
こんにちは、Bさん!
こんにちは、Cさん!
こんにちは、Dさん!
(約1秒後)
挨拶が終わりました
挨拶が終わりました
挨拶が終わりました
挨拶が終わりました
-
greet("A", ...)
が呼ばれます
すぐに「こんにちは、Aさん!」と表示されます -
greet("B", ...)
が呼ばれます
すぐに「こんにちは、Bさん!」と表示されます -
greet("C", ...)
が呼ばれます
すぐに「こんにちは、Cさん!」と表示されます -
greet("D", ...)
が呼ばれます
すぐに「こんにちは、Dさん!」と表示されます -
1秒後に、すべての
setTimeout
が発火し、「挨拶が終わりました」というメッセージもほぼ同時に表示されます
順番に呼ぶため、以下の形に修正しました。
function greet(name, callback) {
setTimeout(() => {
callback();
}, 1000);
console.log(`こんにちは、${name}さん!`);
}
greet("A", function() {
console.log("挨拶が終わりました");
greet("B", function() {
console.log("挨拶が終わりました");
greet("C", function() {
console.log("挨拶が終わりました");
greet("D", function() {
console.log("挨拶が終わりました");
});
});
});
});
- まず
greet("A", ...)
が呼ばれます- すぐに「こんにちは、Aさん!」と表示されます
- 1秒後に、最初のコールバック関数が実行されます
- 1秒後、コールバック内で
greet("B", ...)
が呼ばれます- すぐに「こんにちは、Bさん!」と表示されます
- さらに1秒後に、2番目のコールバック関数が実行されます
- さらに1秒後、次のコールバック内で
greet("C", ...)
が呼ばれます- すぐに「こんにちは、Cさん!」と表示されます
- さらに1秒後に、3番目のコールバック関数が実行されます
- さらに1秒後、最後のコールバック内で
greet("D", ...)
が呼ばれます- すぐに「こんにちは、Dさん!」と表示されます
- さらに1秒後に、最後のコールバック関数が実行されます
入れ子になりすぎて非常に読みづらいです…
複数の非同期処理を連続して行いたい場合、コードが複雑になります
これをコールバック地獄と呼ぶそうです
2. Promise
コールバック地獄を解決するために登場したのが、Promise
Promise
とthen()
を使用することで、入れ子にすることなく、前の処理が完了したら次の処理へ進むことができるようになりました。読みやすい
const greetWithPromise = (name) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("挨拶が終わりました");
resolve();
}, 1000);
console.log(`こんにちは、${name}さん!`);
});
}
greetWithPromise("A")
.then(() => greetWithPromise("B"))
.then(() => greetWithPromise("C"))
.then(() => greetWithPromise("D"));
// 出力
//こんにちは、Aさん!
//挨拶が終わりました
//こんにちは、Bさん!
//挨拶が終わりました
//こんにちは、Cさん!
//挨拶が終わりました
//こんにちは、Dさん!
//挨拶が終わりました
- まず
greetWithPromise("A")
が呼ばれます- すぐに「こんにちは、Aさん!」と表示されます
- 1秒後に、
console.log("挨拶が終わりました");
が実行されます -
resolve()
は非同期関数が成功した場合に呼び出される関数です -
resolve()
でgreetWithPromise("A")のPromiseが完了したことになります
-
.then()
は前のPromise処理の完了したら、then内の関数が呼び出されます
⇒greetWithPromise("B")
が呼ばれます- すぐに「こんにちは、Bさん!」と表示されます
- 1秒後に、
console.log("挨拶が終わりました");
が実行されます -
resolve()
は非同期関数が成功した場合に呼び出される関数です -
resolve()
でgreetWithPromise("B")のPromiseが完了したことになります
- C, Dも同様に処理を行います
3. async/await
Promiseをさらに読みやすく書ける構文
Promiseを受け取った際にthen()
を使用していたところをawait
を使用します。
const greetWithPromise = (name) => {
return new Promise((resolve) => {
console.log(`こんにちは、${name}さん!`);
setTimeout(() => {
console.log("挨拶が終わりました");
resolve();
}, 1000);
});
}
const greet = async () => {
await greetWithPromise("A");
await greetWithPromise("B");
await greetWithPromise("C");
await greetWithPromise("D");
};
greet();
// 出力
//こんにちは、Aさん!
//挨拶が終わりました
//こんにちは、Bさん!
//挨拶が終わりました
//こんにちは、Cさん!
//挨拶が終わりました
//こんにちは、Dさん!
//挨拶が終わりました
おわりに
同期処理と非同期処理の違い、同期処理のデメリット、非同期処理の実現方法について学習しました。
同期処理と非同期処理の違い、同期処理のデメリットは理解できたと感じていますが、非同期処理は使いこなせるかまだ怪しいと感じています。
今後も学習を進めて理解を深めたいです。
参考