nextTick vs queueMicrotask vs Promise.resolve vs setTimeout

## はじめに

普段の開発でなかなかお目にかかることも使うことも滅多にない `queueMicrotask` について興味を持ったついでに、 `process.nextTick` や `Promise`,  `setTimeout` などとの比較をしてみました。

## TL;DR

- 下記の優先度順でキューからタスクが取り出され処理される。
    - `nextTick`
    - `queueMicrotask`, `Promise.resolve`
        - → Microtask
    - `setTimeout`
        - → Task (a.k.a. Macrotask)
- より具体的には、MicrotaskキューとTaskキューの2つのキューがあり、Microtaskキューが優先的に処理されるようになっている。
    - Microtaskキューが空でない限りはTaskキューの中身を実行しない構造となっており、`queueMicrotask`, `Promise.resolve` では Microtaskキューにコールバックを追加し、`setTimeout`等ではTaskキューに追加している。

## サンプルコード

```tsx
const cb = (msg) => () => console.log(msg);

cb('Top level 1')();
process.nextTick(cb('process.nextTick 1'));
queueMicrotask(cb('queueMicrotask 1'));
Promise.resolve().then(cb('Promise 1'));
setTimeout(cb('setTimeout 1'), 0);

setTimeout(cb('setTimeout 2'), 0);
Promise.resolve().then(cb('Promise 2'));
queueMicrotask(cb('queueMicrotask 2'));
process.nextTick(cb('process.nextTick 2'));
cb('Top level 2')();
```

上記のコードを実行した際の標準出力はどうなるでしょう？

結果は以下のようになります。

```shell
Top level 1
Top level 2
process.nextTick 1
process.nextTick 2
queueMicrotask 1
Promise 1
Promise 2
queueMicrotask 2
setTimeout 1
setTimeout 2
```

このことから以下の挙動が考察されます。

1. トップレベルで呼び出されたものは最も早く処理される。
2. `process.nextTick` は `queueMicrotask` や Promise よりも優先的に処理される。
3. `queueMicrotask` と `Promise.resolve` は同じキューを使用しており、登録順に処理されている。
    1. Microtaskキューに追加されている。
       **Q1. Microtask内で新たにMicrotaskを追加した場合はどうなるのか？**
4. `setTimeout` は `queueMicrotask` や `Promise.resolve` のコールバックをすべて実行したあとに呼び出されている。
    1. → Taskキューに追加されている。
    2. → Taskキューの中身の実行は、Microtaskキューの中身が空になってから行われる。
        **Q2. Task内でMicrotaskを追加した場合はどうなるのか？**

 

### Q1. Microtask内で新たにMicrotaskを追加した場合はどうなるのか？

**結論: Microtask実行後、Taskの実行へと移行せず、新たに追加したMicrotaskを実行する。**

```tsx
const cb = (msg) => () => {
    console.log(msg)
};

// Taskキューにコールバックを追加
setTimeout(cb("Task"), 0);

// Microtaskキューにコールバックを追加
queueMicrotask(() => {
    console.log("queueMicrotask 1");

    // Microtask内で更にMicrotaskキューにコールバックを追加
    queueMicrotask(() => {
        console.log("queueMicrotask 2");
    })
})
```

標準出力

```shell
queueMicrotask 1
queueMicrotask 2
Task
```

上記の結果からも分かる通り、無限ループの可能性がある。

Taskキューの実行にはMicrotaskキューが空になる必要があることから、`setTimeout`などの後続の処理の実行を止めてしまう可能性があるため、使用する際には注意する必要がある。

### Q2. Task内でMicrotaskを追加した場合はどうなるのか？

**結論: Taskが完了してからMicrotaskが実行される。**

```tsx
// Taskキューにコールバックを追加
setTimeout(() => {
    console.log('setTimeout');

    // Task内でMicrotaskを追加
    queueMicrotask(() => {
        console.log("queueMicrotask 1");
    })
}, 0);
```

標準出力

```shell
setTimeout
queueMicrotask 1
```

### Q3. で、`queueMicrotask`はどんな用途があるの？

**結論: 非同期処理を模したい場合や、バッチ処理の用途がありそう。でも普段の開発ではめったに使わないかも。**

非同期処理を模したいケースとして、MDNに乗っていたサンプルをそのまま利用し説明します。

下記のコードでは、 `getData` というプロパティに、キャッシュがあれば同期的にイベントをディスパッチし、無ければ非同期的に情報を取得するというハンドラーをセットしています。

```tsx
customElement.prototype.getData = (url) => {
    if (this.cache[url]) {
        this.data = this.cache[url];
        this.dispatchEvent(new Event("load"));
    } else {
        fetch(url)
        	.then((result) => result.arrayBuffer())
    		.then((data) => {
                this.cache[url] = data;
                this.data = data;
                this.dispatchEvent(new Event("load"));
            });
	}
};
```

この場合、以下のコードを実行した際の結果は、キャッシュがある場合と無い場合で変わってきます。

```tsx
element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data…");
element.getData();
console.log("Data fetched");
```

キャッシュが**無い場合**、else句に入り非同期処理を行うため内部ではmicrotaskへの追加はを行っています。よって標準出力は以下の通りになります。

```shell
Fetching data
Data fetched
Loaded data
```

`Data fetched`の後に、 `Loaded data` が表示されています。

一方でキャッシュがある場合、if文の中に入り同期的にイベントのディスパッチを行うため、よって以下の標準出力になります。

```shell
Fetching data
Loaded data
Data fetched
```

`Loaded data` が `Data fetched` よりも先に表示されていることが分かります。

これは同期的に処理を行うため順序が異なってしまっており、Fetchedの前にLoadedと表示される不思議な結果となってしまっています。

そこで非同期的に処理を行いつつ、かつなるべく早くイベントをディスパッチするために、 `queueMicrotask` が役に立ちます。つまり、以下のように変更を加えることで想定する動作を達成することが出来ます。

```tsx
customElement.prototype.getData = (url) => {
	if (this.cache[url]) {
		queueMicrotask(() => {
			this.data = this.cache[url];
	    this.dispatchEvent(new Event("load"));
		});
  } else {
    fetch(url)
			.then((result) => result.arrayBuffer())
			.then((data) => {
	      this.cache[url] = data;
	      this.data = data;
	      this.dispatchEvent(new Event("load"));
	  });
	}
};
```

## 参考資料

[queueMicrotask() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask)

[Using microtasks in JavaScript with queueMicrotask() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide)