この記事はOPENLOGI Advent Calendar 2022の14日目の記事です。
はじめに
私は新卒でSIerで2年ほど働いた後にオープンロジに今年の5月に入社し、バックエンド(PHP/Laravel)、フロントエンド(JavaScript/React)で開発をしています。
前職はバックエンドの開発経験が多くフロントエンドの経験があまりない状態で入社したのですが、入社後にフロントエンドの開発でJavaScriptを触る上で躓いたことの一つに同期/非同期処理があります。
JavaScriptで非同期処理というと、PromiseやAsync/Awaitなどを思い浮かべる方も多いと思います。
私もコードを触る中でPromiseやAsync/Awaitを見る機会があったのでそれらの使い方について調べていたのですが、調べていくうちにPromiseやAsync/Awaitを理解するにはまずはJavaScriptの同期/非同期処理とはどういったものか知る必要があることに気づきました。
これまでは細かい修正が多かったのでなんとかなっていましたが、今後のためにもまずはJavaScriptの同期/非同期処理について整理していきたいと思います!
同期処理とは
同期処理はコードの流れの順に実際に処理が実行されるという一般的な処理の流れになります。
言い換えると処理の実行順が担保されている処理とも言えるかと思います。
以下のコードを例にみていきます。
function task1(){
console.log('task1');
}
function task2(){
console.log('task2');
}
function task3(){
console.log('task3');
}
function main(){
task1();
task2();
task3();
}
main();
- 実行結果
task1
task2
task3
コードの順番通りにtask1→task2→task3の順番で出力されています。
task1終了の同期をとってtask2を実行、task2終了の同期をとってtask3を実行してるので同期処理と呼ばれるイメージになるかと思います。
コールスタック
上記の同期処理の流れは一般的な処理の流れになり特に変わった点はありませんが、
この後の非同期処理と比較するためにはコールスタックについて理解する必要があります。
コールスタックとは実行する関数の情報について蓄積している場所のことを言います。
関数の実行は後に呼び出した関数の方が先に実行されるため、後入先出となりスタックと呼ばれています。
実行イメージ
上記の実行例のコードの場合は、コールスタックは以下のような推移となるイメージです。
- main()を呼び出し時にコールスタックに追加
- task1()を呼び出し時にコールスタックに追加
"task1"を出力
- task1()を終了時にコールスタックから削除
- task2()を呼び出し時コールスタックに追加
"task2"を出力
- task2()を終了時にコールスタックから削除
- task3()を呼び出し時にコールスタックに追加
"task3"を出力
- task3()を終了時にコールスタックから削除
- main()を終了時にコールスタックから削除
非同期処理とは
非同期処理とは、コードの流れとは異なる順番で実際の処理がされることを言います。
言い換えれば処理の実行順が担保されていない処理とも言えるかと思います。
JavaScriptでは例としてsetTimeout関数を使うことで非同期処理を実現することができます。
以下はtask2関数の実行を非同期化したコードとなります。
function task1(){
console.log('task1');
}
function task2(){
console.log('task2');
}
function task3(){
console.log('task3');
}
function main(){
task1();
setTimeout(() => task2(), 1);
task3();
}
main();
- 実行結果
task1
task3
task2
コードの順番はtask1→task2→task3ですが、task1→task3→task2の順番で出力されています。
task3はtask2終了の同期をとらずに先に実行されているので非同期処理と呼ばれるイメージになるかと思います。
タスクキュー
非同期処理の場合は、同期処理の時に登場したコールスタックの他にタスクキューという概念が新たに登場します。
タスクキューとはコールスタックとは別に非同期で実行する処理に関する情報を蓄積する場所のことを言います。
先に積まれたタスクが先に実行されるので先入先出となりこちらはキューと呼ばれています。
setTimeout関数では第1引数のコールバック関数で指定した処理をタスクキューに積むことで非同期処理を実現しています。
タスクキューはイベントループという仕組みでコールスタックをチェックしており、コールスタックが空になると先入先出でタスクキューに積まれたタスクが順次コールスタックに積まれる形となります。
setTimeout関数の第2引数はタスクキューに積まれるまでの時間であり、実際に処理が実行されるまでの時間ではないという点に注意が必要です。そのため指定時間経過後にタスクキューにタスクが積まれていたとしても、コールスタックが空ではない場合は空になるまで待った後に処理が実行されるということになります。
個人的にはsetTimeoutよりもdispatchなどの名前の方が処理内容のイメージがつきやすいと思っています。
WebAPIs
上記でsetTimeout関数の第2引数はタスクキューに積まれるまでの時間を指定すると書きましたが、この処理は実はWebAPIsと呼ばれるものを使用しています。
WebAPIsとは何かの前に、そもそもブラウザ上ではJSエンジンを搭載していることによりJavaScriptのコードを実行でき、例えばChromeだとV8 JavaScript engineが搭載されておりJavaScriptのコードを機械語に変換して実行しています。
そして、ブラウザはJSエンジンを搭載しているだけではなくその他にもWebAPIsと呼ばれるAPIを備えており、setTimeout関数はWebAPIsの一部ということになります。
setTimeout関数を使うときにAPIであることを意識しなくても使えるのでイメージがつきにくいですが、コールスタックが同期処理をしている中でSetTimout関数が指定時間待機してからタスクキューに積むという処理ができるのはWebAPIsを利用しているから可能になる、というイメージになるかと思います。
参考
- Understanding JavaScript Call Stack, Task Queue and Event Loop | by Azmain Amin | JavaScript in Plain English
- setTimeout and the Web Api
- Regular Event Loop
実行イメージ
上記の実行例のコードの場合は、コールスタックとタスクキューは以下のような推移となるイメージです。
- main()を呼び出し時にコールスタックに追加
- task1()を呼び出し時にコールスタックに追加
"task1"を出力
- task1()を終了時にコールスタックから削除
- setTimeout()を呼び出し時にコールスタックに追加
- task2()をWebAPIsに追加
- setTimeout()を終了時にコールスタックから削除
- 1ms秒待機後にtask2()をタスクキューに追加(正確には順番前後する可能性あり)
- task3()を呼び出し時にコールスタックに追加
"task3"を出力
コールスタックが空ではないのでtask2はタスクキューに置かれたまま
- task3()を終了時にコールスタックから削除
コールスタックが空ではないのでtask2はタスクキューに置かれたまま
- main()を終了時にコールスタックから削除
コールスタックが空になったのでtask2はコールスタックに追加することができる
- task2()をコールスタックに追加
"task2"を出力
私自身、非同期処理を裏側で何か別の処理を実行させる並列処理と混同していたことがあったのですが、JavaScriptの場合はシングルスレッドであり二つの処理を並列させて実行することはできません。
あくまで同期処理が終了した(=コールスタックが空になった)後に直列的に非同期処理(=task2)が実行される点に注意が必要です。
まとめ
本記事ではJavaScriptの同期/非同期処理についてPromiseやAsync/Awaitを知る前に最低限知っておいた方がいいと思うことについてまとめました。
より踏み込んだPromiseやAsync/Awaitについてもまた別途記事を書ければと思ってます!
————————————
【オープンロジイベント情報】
<12/15(木)19:30〜>
「CTO・VPoEぶっちゃけトーク! 〜失敗から学ぶエンジニア組織論〜」
過去の失敗談をセキララに語りつつ、オープンロジでどんな組織をつくっていくかが語られる予定なので、ご都合合う方は是非ご参加ください!
https://openlogi.connpass.com/event/265230/
————————————
参考
- JavaScriptのイベントループまわりは、どういう仕組みで動いているのか?
- Call stack (コールスタック) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
- JavaScript で queueMicrotask() によるマイクロタスクの使用 - Web API | MDN
- Understanding JavaScript Call Stack, Task Queue and Event Loop | by Azmain Amin | JavaScript in Plain English
- setTimeout and the Web Api
- Regular Event Loop