非同期のJavaScriptは、長時間かかる操作を行う際に、
ブラウザが他の作業をブロックせずに続行できるようにするための重要な概念です。
これにより、ユーザーインターフェースがスムーズに動作し、応答性が高まります。非同期処理の主な方法には、コールバック関数、プロミス、async/awaitがあります。
今回は、非同期処理の考え方とコールバック関数についてまとめていこうと思います。
目次:
コールスタックとは?
JavaScriptが裏側で使っている仕組みの一つです。
これを知ることで、JavaScriptがどのように関数の呼び出しを管理しているのか、
そして非同期処理の理解も深まります。
MDN 参照:
インタープリター(Webブラウザ内のJavaScripインタープリターなど)の仕組みの一つで、複数階層の関数を呼び出したスクリプト内の位置を追跡し続けること。
スタックとは、データ構造の名前になります。
最後に入れたものが、最初に取り出されるというデータ構造のことです。
英語だと、lastin firstoutと言います。
どの関数が現在実行されているのか、その関数の中でどの関数が呼び出されたか、などを表します。
仕組み
・スクリプトが関数を呼び出すとき、インタープリターはそれをコールスタックに追加し、それから関数の実行を始める。
・その関数から呼び出されたどの関数も、コールスタックのその上に追加され、呼び出し先を実行する
・現在の関数が終了すると、インタープリターはスタックからそれを外し、最後のコードがリストされている場所から実行を再開する。
関数の実行を例にコールスタックについて考えてみましょう。
const multiply = (x, y) => x * y;
const square = (x) => multiply(x, x);
const isRightTriangle = (a, b, c) => {
return square(a) + square(b) === square(c);
};
関数isRightTriangleは、
square(a)とsquare(b)で呼び出したものを足して、それがsquare(c)と等しいかどうかを比較しています。
また、square関数は、関数内でmultiplyというものを呼び、
multiplyは、渡された値を掛け算するシンプルなものになります。
つまり、square関数は、2乗します。
isRightTriangle(3, 4, 5);
isRightTriangle(3, 4, 8);
true
false
つまりこのコードは、与えられた3つの辺a, b, cが直角三角形の辺かどうかを確認します。
ここで、コールスタックがどのように機能するかを見ていきましょう。
コールスタックの追跡
ステップ1: console.log (isRightTriangle(3, 4, 5))
isRightTriangle(3, 4, 5)を呼び出すために、isRightTriangle関数がコールスタックにプッシュされます。
コールスタック: [isRightTriangle]
ステップ2: isRightTriangle(3, 4, 5)
isRightTriangle関数の実行が開始されます。square(a)とsquare(b)が呼び出されます。
まず、square(3)が呼び出されるため、square関数がコールスタックにプッシュされます。
コールスタック: [isRightTriangle, square]
ステップ3: square(3)
square関数の実行が開始され、multiply(3, 3)が呼び出されます。
multiply関数がコールスタックにプッシュされます。
コールスタック: [isRightTriangle, square, multiply]
ステップ4: multiply(3, 3)
multiply関数が実行されます。3 * 3が計算され、結果9が返されます。
multiply関数がコールスタックからポップされます。
コールスタック: [isRightTriangle, square]
ステップ5: square(3)の戻り
square関数が結果9を返します。
square関数がコールスタックからポップされます。
コールスタック: [isRightTriangle]
ステップ6: square(4)
次に、square(4)が呼び出され、square関数が再びコールスタックにプッシュされます。
コールスタック: [isRightTriangle, square]
・・・
コールスタック: [isRightTriangle]
ステップ14: isRightTriangleの戻り
isRightTriangle関数が戻り値trueを返します。これは、9 + 16 === 25がtrueであるためです。
isRightTriangle関数がコールスタックからポップされます。
コールスタック: []
ステップ15: console.log
console.logが結果trueを出力します。
コールスタックには最終的に何も残りません。
コールスタックの動作
この流れを通じて、関数が呼び出されるたびにコールスタックにプッシュされ、
実行が完了するとポップされることがわかります。
コールスタックは常に現在の実行コンテキストを保持し、最も上にある関数から順に実行されます。
コールスタックはJavaScriptのシングルスレッドの特性を反映しており、
シングルスレッドでは一度に一つの命令しか実行できません。
そのため、非同期処理を使用することで、ブロッキングを避けて効率的に重たい操作を処理することができます。
シングルスレッドとは?
JavaScriptはシングルスレッドで動作します。つまり、一度に一つの作業しかできません。ある瞬間において一つのことしかしていないという意味です。
それでは、JavaScriptで時間のかかる処理をする場合、しばらく待たなければいけないのでしょうか?
A. コールバック関数を使用する。
特定の処理を検知すると、JavaScripはブラウザにその処理を委譲します。
ブラウザはweb API(JavaScripから呼び出せる大量のメソッドのようなもの)と呼ばれるバックグラウンドで処理を実行してくれる機能(例: HTTPリクエストやsetTimeout)を提供してくれます。
・JavaScripのコールスタックはこのweb APIを認識すると、ブラウザに処理を依頼します。
・ブラウザが処理を終えると、コールバック関数がイベントキューに追加され、コールスタックが空になるのを待ってから実行されます。
まとめ:
ブラウザにはたくさんのメソッドが用意されていて、それを使うことでJavaScriptはブラウザに処理を依頼することができる。
そのメソッドにコールバック関数を渡してあげることで、ブラウザの処理が終わった後に実行するものを指定することができる。
関数そのものをコールバック関数として渡していることにより、しかるべきタイミングでその関数を呼び出すことができる。
このように、JavaScriptのシングルスレッドの制約を補うために、Web APIとイベントループを活用して非同期処理を実現しています。
ブラウザがバックグラウンドで処理を行い、その結果をコールバック関数で受け取ることで、JavaScriptのメインスレッドをブロックすることなく、効率的に複数の処理を同時に扱うことができます。
コールバックに関するデモ
色を1秒ごとに背景色として出力してみる。
スタイルを変更
document.body.style.backgroundColor = 'red';
document.body.style.backgroundColor = 'orange';
setTimeoutで指定した秒ごとに色を変更します。
setTimeout(() => {
document.body.style.backgroundColor = 'red';
},1000);
setTimeout(() => {
document.body.style.backgroundColor = 'orange';
},2000);
setTimeout(() => {
document.body.style.backgroundColor = 'yellow';
},3000);
気合いで背景色を変更することはできるが、
・コードの意味が分かりずらい
・出力するための計算が必要
つまり面倒です・・・・
そこで、背景色を順番に変えていくJavaScriptを記載してみます。
解決方法:
セットタイムアウトをネストする
セットタイムアウトを中で呼ぶことで、1秒後に赤、その1秒後にオレンジその1秒後に黄色と、色を変更することができます。
setTimeout(() => {
document.body.style.backgroundColor = 'red';
setTimeout(() => {
document.body.style.backgroundColor = 'orange';
setTimeout(() => {
document.body.style.backgroundColor = 'yellow';
},1000);
},1000);
},1000);
また、greenとblueも1秒後に表示するように修正してみます。
setTimeout(() => {
document.body.style.backgroundColor = 'red';
setTimeout(() => {
document.body.style.backgroundColor = 'orange';
setTimeout(() => {
document.body.style.backgroundColor = 'yellow';
setTimeout(() => {
document.body.style.backgroundColor = 'green';
setTimeout(() => {
document.body.style.backgroundColor = 'blue';
},1000);
},1000);
},1000);
},1000);
},1000);
自分で作った関数でも同じ操作をすることが可能です。
関数delayColorChangeを作成します。
const delayedColorChange = (newColor, delay, doNext) => {
setTimeout(() => {
document.body.style.backgroundColor = newColor;
doNext();
},delay);
}
関数のロジック:
- パラメータとして、newColor、delay、doNextを受け取ります。
- setTimeoutを使用し、色は、newColorで受け取った値を使います。
- donext()は、高階関数。自分の処理が終わった後に、受け取った関数を実行します。
- delayは、setTimeoutで待つ待ち時間を受け取ります。
delayedColorChange('red', 1000, () => {
delayedColorChange('orange', 1000, () => {
delayedColorChange('yellow', 1000, () => {
delayedColorChange('green', 1000, () => {
delayedColorChange('blue', 1000, () => {
});
});
});
});
});
delayedColorChangeに色を表す値と待ち時間delayとdoNextを実行します。
いつ終わるかわからないコード(非同期なコード)があり、そのコードが終わってから次のコードを実行したい場合は、必ずコールバック関数が関わってきます。
上記のように、コールバック関数を複数ネストして書くことは多々あります。
この状態をコールバック地獄と呼びます。
しかし、複数のコールバックを書くことにより、
コードが見えづらくなり、記入漏れやエラーの発生の原因となってしまう可能性があります。
このコールバック地獄を解決してくれるのが、promiseやasync、awaitになります。
こちらの解決方法については、別の記事でまとめていこうと思います。