Javascript 同期処理と非同期処理の内部処理について

Javascriptの同期処理と非同期処理が内部でどのように処理されているかについて、深掘りした内容をまとめてみました。
なお、同期処理と非同期処理に関する基本的な知識は前提としています。

## 同期処理について
まずは、同期処理について2つのケースを考えてみます。

### Case 1
```js
function() sync1() {
    console.log('sync1');
}

function() sync2() {
    console.log('sync2');
}

function() sync3() {
    console.log('sync3');
}

sync1();
sync2();
sync3();

/* 
-- 出力 --
'sync1' --- ①
'sync2' --- ②
'sync3' --- ③
---------
*/
```
上記は、単に同期関数を連続して3つ呼び出しただけの、最もシンプルなケースです。
この場合の内部処理は以下のようになっています。
|1.初期状態|2.`sync1`がCallStackにpushされ実行 & CallStackからpop（①）|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/a212e3cf-948f-66bd-3336-fc02f63585f9.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/5323c30a-7ab7-29ad-9ce7-47fb45c41fe5.png)|
|3.`sync2`がCallStackにpushされ実行 & CallStackからpop（②）|4.`sync3`がCallStackにpushされ実行 & CallStackからpop（③）|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/f72cbdc5-ad40-ef2f-f2da-ed887637f68e.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/efdf0ecb-8cc7-19e5-ace6-26ec7a8521a9.png)|

関数は初め、待機状態となっていて、関数が呼び出されるとCallStackと呼ばれる場所に移動（push）され、実行されます。また、実行し終わった関数はCallStackから取り除かれます（pop）。
JavaScriptではCallStackで関数が実行される、ということを覚えていてください。
また、非同期関数の場合は、これとは異なる挙動を示しますが、それは後ほど説明します。

:::note info
CallStack = 関数の実行される場所
:::


### Case 2
```js
function sync1() {
    sync2();
    console.log('sync1');
}

function sync2() {
    sync3();
    console.log('sync2');
}

function sync3() {
    console.log('sync3');
}

sync1();

/* 
-- 出力 --
'sync3' --- ①
'sync2' --- ②
'sync1' --- ③
---------
*/
```
今度は、`sync1`の中で`sync2`を呼び出し、さらにその中で`sync3`を呼び出しています。この場合の内部処理は以下のようになっています。
|1.初期状態|2.`sync1`がCallStackにpushされ実行|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/bbd3f03f-2428-e06c-59fc-afcc5825ffe7.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/dff1a2c6-2157-d1e1-1cf6-5a4b2f3e2d46.png)|
|3.`sync1`の実行中に`sync2`がCallStackにpushされ実行|4.`sync2`の実行中に`sync3`がされCallStackにpushされ実行 & `sync3`がCallStackからpop（①）|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/c9666e75-dbf5-678d-c868-9dd1feb43824.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/d04caf34-86d2-457f-b34c-9402591cfe79.png)|
|5.`sync2`がCallStackからpop（②）|6.`sync1`がCallStackからpop（③）|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/d7468d9a-5ce9-6679-5097-b94288be63ba.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/61c9756a-8e02-4779-1869-d7ce4636903f.png)|

CallStackは名前からも推測できる通り、スタック型のデータ構造をしており、後入れ先出し（LIFO：Last In, First Out）方式で処理されます。すなわち、CallStackに最後にpushされた`sync3`から処理されていくというわけです。つまり、関数のネストが深ければ深いほど、CallStackに溜まっていくことになります。
ここまでの2つのCaseで、ある程度同期処理の流れが掴めたかと思います。

## 非同期処理について
次に、非同期関数も追加して考えてみます。ここでも、2つのケースを用意しました。

### Case 1
```js
function async1() {
    setTimeout(() => {
        console.log('async1');
    }, 0)
}

function sync1() {
    console.log('sync1');
}

function async2() {
    setTimeout(() => {
        console.log('async2');
    }, 0)
}

function sync2() {
    console.log('sync2');
}

async1();
sync1();
async2();
sync2();

/* 
-- 出力 --
'sync1' --- ①
'sync2' --- ②
'async1' --- ③
'async2' --- ④
---------
*/
```
非同期関数と同期関数を交互に呼び出しています。この場合の内部処理は以下のようになっています。

|1.初期状態|2.`async1`がMacroTaskにpush|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/62e52359-b779-302c-4b9c-c51ea4cb00db.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/635ac1cd-ab28-c74e-82a8-777fd2101c82.png)|
|3.`sync1`がCallStackにpushされ実行 & `sync1`がCallStackからpop（①）|4.`async2`がMacroTaskにpush|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/8337a702-5c79-7af5-d69f-6ebb795787c7.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/c6a27ad8-96cf-dbfe-5274-ebb8992836fb.png)|
|5.`sync2`がCallStackにpushされ実行 & `sync2`がCallStackからpop（②）|6.`async1`がCallStackにpushされ実行 & `async1`がCallStackからpop（③）|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/1e478d87-de20-6acd-45a2-7a44088d0869.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/7dee83db-9c7a-790d-c565-c39fe6182ad4.png)|
|7.`async2`がCallStackにpushされ実行 & `async2`がCallStackからpop（④）||
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/0484d0fc-265b-b3bf-8449-ef2821a3952d.png)||

`setTimeout`でラップされた関数は遅延の時間に関係なく、非同期処理として扱われます。非同期処理には2種類存在し、MacroTaskにpushされる非同期処理と、MicroTaskにpushされる非同期処理です（MicroTaskにpushされる非同期処理についてはCase 2で説明します）。
`setTimeout`はMacroTaskにpushされる非同期関数なため、`async1`, `async2`はいきなりCallStackにpushされるのではなく、まずMacroTaskにpushされます。MacroTaskに積まれた非同期関数は、CallStackに積まれている同期関数が全て実行し終えた後で、CallStackにpushされ実行されます。したがって、上で示した図のような挙動を示し、同期関数が全て実行された後で、非同期関数が実行されるというわけです。

### Case 2
```js
function async1() {
    setTimeout(() => {
        console.log('async1');
    }, 0)
}

function sync1() {
    console.log('sync1');
}

async function async2() {
    Promise.resolve().then(() => {
        console.log('async2');
    });
}

function sync2() {
    console.log('sync2');
}

async1();
sync1();
async2();
sync2();

/* 
-- 出力 --
'sync1' --- ①
'sync2' --- ②
'async2' --- ③
'async1' --- ④
---------
*/
```
同様に非同期関数と同期関数を交互に呼び出していますが、出力結果が前回とは異なっています。この場合の内部処理は以下のようになっています。

|1.初期状態|2.`async1`がMacroTaskにpush|
|:-:|:-:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/62e52359-b779-302c-4b9c-c51ea4cb00db.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/635ac1cd-ab28-c74e-82a8-777fd2101c82.png)|
|3.`sync1`がCallStackにpushされ実行 & `sync1`がCallStackからpop（①）|4.`async2`がMicroTaskにpush|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/8337a702-5c79-7af5-d69f-6ebb795787c7.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/b1873e87-9bc0-0dfc-937c-be96cb20d2e6.png)|
|5.`sync2`がCallStackにpushされ実行 & `sync2`がCallStackからpop（②）|6.`async2`がCallStackにpushされ実行 & `async2`がCallStackからpop（③）|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/bdf14f9c-045c-077c-fe5d-b493a4a6915a.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/a9ffd210-5838-119f-7d7a-a1d194c05a14.png)|
|7.`async1`がCallStackにpushされ実行 & `async1`がCallStackからpop（④）||
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3908899/1b7ef52a-4061-c197-f9e4-a394062383c6.png)||

なんとなく予想はつくかと思いますが、Promiseで解決された非同期処理はMicroTaskにpushされます。そして、MicroTaskに積まれた関数も同様に、CallStackに積まれている同期関数が全て実行し終えた後で、CallStackにpushされ実行されます。
また、MicroTaskとMacroTaskでは<b>MicroTaskに積まれた関数が優先してCallStackにpushされます。</b>すなわち、MicroTaskの非同期関数が全て実行された後で、MacroTaskに積まれた非同期関数がCallStackにpushされ、最後に実行されるということになります。したがって、上で示した図のような挙動を示し、`async1`と`async2`の実行順序が逆転するというわけです。
ちなみに、MicroTaskとMacroTaskは、キュー型のデータ構造で、先入れ先出し（FIFO：First In, First Out）方式で処理されます。

## まとめ
ここまでの内容をまとめると以下のようになります。

:::note info
1. 同期関数 -> CallStackにpush -> ただちに実行
2. 非同期関数（Promise系）-> MicroTaskにpush -> 1の後に実行
3. 非同期関数（setTimeout系）-> MacroTaskにpush -> 2の後に実行
:::

また、MacroTask, MicroTaskに追加される非同期関数には他にも以下のものがあるようです。

MacroTask: `setInterval()`, `setImmediate()`, `I/O`操作（ファイルの読み書き）
MicroTask: `process.nextTick()`, `MutationObserver`

## 複雑な例
最後に、今までよりも複雑なケースを考えてより理解を深めてみたいと思います。以下の場合について、どのような順序でログが出力されるか考えてみてください。
```js
function async1() {
    console.log('async1 start');
    setTimeout(() => {
        console.log('setTimeout 1');
        Promise.resolve().then(() => {
            console.log('setTimeout 1 microtask');
        });
    }, 0);

    Promise.resolve().then(() => {
        console.log('async1 microtask');
    });

    console.log('async1 end');
}

async1();
```
答えは、以下のようになります。

```js
/*
-- 出力 --
'async1 start'
'async1 end'
'async1 microtask'
'setTimeout 1'
'setTimeout 1 microtask'
---------
*/
```