👽

非同期処理についてまとめてみた

2022/09/23に公開約5,500字

スレッド

以下のような、連続して順番に実行される一本の処理の流れ。

ブラウザのスレッドの種類

ブラウザには、メインスレッド(Main thread)サービス・ワーカー(Service Worker)
ウェブワーカー(Web Worker)などのスレッドがある。

この中でも、主にJavaScriptはメインスレッド(Main thread)で実行される。

メインスレッド(Main thread)は、ブラウザがユーザーのイベントや描画を処理するところ。

JavaScriptの実行やレンダリングはメインスレッド上で行われる。

その流れの中で、JavaScriptの処理が重かったり、レンダリングが重い場合などは、
60fps=16.7m秒に一回画面更新を下回ってしまうため、画面のがたつきやちらつきが発生してしまう可能性もある。

そのため、コードの処理時間を短くしたり、レンダリングの負荷が少ないCSSの記述に変更したりなどして対応する必要がある。

フレームレート(fps)とは?

一秒間あたりの画面更新頻度。
画面更新頻度が多くなるほど、滑らかな表示を実現することができる。
60fps=16.7m秒に一回画面更新が出ていれば人の目にはスムーズに見ることができる。
(液晶テレビなどが60fps)

同期処理・非同期処理

同期処理

  • メインスレッドで順番にコードが実行される。
  • 一つの処理が完了するまでは次の処理には進むことができない。
    (重い処理などがある場合は、その処理を待つ必要がある)

コードで確認してみる。

sleep関数に3000を渡して、3秒間メインスレッドを占有して確認をしてみます。

function sleep(wait) {
  const startTime = new Date();
  while (new Date() - startTime < wait);
  console.log("3秒間メインスレッドを占有しました。")
}

sleep(3000);

console.log("実行されました!");

実行結果

3秒経過後に、実行されました!と出力。
ある処理が行われている場合には、その処理の完了をまってから次の処理が実行されるということが分かりました。

非同期処理

非同期処理は一時的にメインスレッドから処理が切り離される
(その間に他の処理をすることが可能となる)

まずsetTimeoutの第二引数に1000を渡し、1秒間待ってからsleep関数を実行したときの挙動を確認してみます。

function sleep(wait) {
  const startTime = new Date();
  while (new Date() - startTime < wait);
  console.log("3秒間メインスレッドを占有しました。")
}

setTimeout(()=>{
  sleep(3000);
},1000);

console.log("実行されました!");

実行結果

すぐに実行されました!と出力されることが確認できました。
setTimeoutに渡したコールバック関数が非同期処理として、メインスレッドから1秒間だけ切り離されたことによって、その間に他の処理をすることが可能となったため。

1秒経過後には再び3秒間メインスレッドがsleep関数によって占有される。

タスクキュー

実行待ちになっている非同期処理の行列のこと。
キューというのが実行待ちの行列を表していて、そのなかでタスクを管理しているイメージ。

このような仕組みを、先入れ先出し(FIFO (First In, First Out))と呼ぶ。
キューに入ってきたものから順番に処理をする。

このタスクキューがコールスタックと連携することによって、非同期処理の実行順を決めている。

コールスタックとは?

  • 実行中のコードがたどってきたコンテキストの積み重ね
  • コードが始まるときに、グローバルコンテキストが生成される
  • 一番上に積まれているコンテキストが実行中のものである
  • 処理が終了した関数のコンテキストはコールスタックから消滅する

以下のコードを使用して、開発ツールで確認してみるとコンテキストが積み上げられていることが確認できました。

function bar() {}
function Foo() {
  bar();
}

Foo();

anonymousはグローバルコンテキスト

コードで確認してみる。

function foo() {
  setTimeout(function task1() {
    console.log("task1が実行されました。");
  }, 4000);

  const startTime = new Date();
  while (new Date() - startTime < 2000);

  console.log("関数fooが実行されました。");
}

foo();

console.log("task2が実行されました。");

実行結果

処理は以下の流れになっていることが分かります。

  1. 関数fooがメインスレッドを2秒間占有後に、関数fooが実行されました。
  2. 関数fooの処理を待ってtask2が実行されました。と出力
  3. 4秒経過後に、task1が実行されました。と出力

では次に、メインスレッドを占有している時間をsetTimeoutの第二引数で指定している4秒間より多い8秒間にした場合はどうなるのかを確認してみます。

function foo() {
  setTimeout(function task1() {
    console.log("task1が実行されました。");
  }, 4000);

  const startTime = new Date();
  while (new Date() - startTime < 8000);

  console.log("関数fooが実行されました。");
}

foo();

console.log("task2が実行されました。");

実行結果

実行結果を見てみると、先ほどと全く変わっていないことが分かります。

なぜこのような結果になるかを順番に整理してみます。

  1. スクリプトの実行がされると、コールスタックにグローバルコンテキストが生成される。
    (グローバルコンテキストの中にはtask2関数foo)
    関数fooが実行されて関数コンテキストが生成される。
    このときtask1はsetTimeoutが呼ばれて4秒間待機している。
    関数fooの中で8秒間待機している。
  2. task1関数fooが待機している間に、task2がタスクキューに渡される。
  3. 4秒間待機していた、task1がタスクキューに渡される。
    (このときのタスクキューに渡された順番は、task2task1となっている)
  4. 関数foo5秒間の待機が終わって実行されると、コールスタックからグローバルコンテキストも含めて消える。このときにコールスタック内が初めて空になる。
    この段階で関数fooが実行されました。と出力
  5. イベントルーブがタスクキューに対してコールスタックが空いたことを伝える。
    task2が実行されました。と出力
  6. task2が実行されて、再びコールスタックに空になり、task1が渡されて実行される。
    task1が実行されました。と出力

この流れからタスクキューに入った順番で非同期処理が実行されていることが分かる。

※メインスレッドが占有されている状態とは、コールスタックにコンテキストが積まれている状態と同じ意味になる。

イベントループとは?

コールスタックにコンテキストが積まれているかどうかを定期的に確認している。
空いている場合は、タスクキューに空きがあることを伝えくれる。
空きがあることが分かったタスクキューはコールスタックにタスクを渡す。

コールバック関数と非同期処理

コードで確認してみる

function foo() {
  setTimeout(function task1() {
    console.log("task1が実行されました。");
  });

  console.log("関数fooが実行されました。");
}

function bar() {
  console.log("関数barが実行されました。");
}

foo();

bar();

実行結果

setTimeoutの第二引数も指定していないので、foo()→bar()順番に実行されるのかと思われますが、そうではないようです。

処理の流れを整理してみます。

  1. スクリプトが実行されるとグローバルコンテキストから関数fooが実行される。
    ここで、関数fooが実行されました。と出力
  2. 関数fooでsetTimeoutが呼ばれてタスクキューにtask1が渡される。
  3. 関数fooがコールスタックから消えて、関数barがコールスタックに積まれて実行される。
    (このときはコールスタックが空になっていない為、task1はタスクキューにまだ残ったまま)
    ここで、関数barが実行されました。と出力
  4. 最後にtask1が実行されて、task1が実行されました。と出力

関数barをtask1の後に呼ぶにはどうすればよいか?

記述を変更してみる

function foo() {
  setTimeout(function task1() {
    console.log("task1が実行されました。");
    bar();
  });

  console.log("関数fooが実行されました。");
}

function bar() {
  console.log("関数barが実行されました。");
}

foo();

// bar();

実行結果

処理の流れを整理してみます。

  1. スクリプトが実行されるとグローバルコンテキストから関数fooが実行される。
    非同期APIを通じてsetTimeoutを渡すときに関数bartask1の中で実行される関数として渡す。
    ここで、関数fooが実行されました。と出力
  2. タスクキューに関数barを含んだtask1が渡される。
  3. コールスタックが空になると、task1から関数barが実行される。

このような特徴を使うことによって、特定の非同期処理のあとに特定の関数を実行することができる。

参考文献

この動画を参考にしながら挙動を確認していきました。

https://www.udemy.com/course/javascript-essence/

Discussion

ログインするとコメントできます