8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nextTick vs queueMicrotask vs Promise.resolve vs setTimeout

Posted at

はじめに

普段の開発でなかなかお目にかかることも使うことも滅多にない queueMicrotask について興味を持ったついでに、 process.nextTickPromise, 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キューに追加している。

サンプルコード

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')();

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

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

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.nextTickqueueMicrotask や Promise よりも優先的に処理される。
  3. queueMicrotaskPromise.resolve は同じキューを使用しており、登録順に処理されている。
    1. Microtaskキューに追加されている。
      Q1. Microtask内で新たにMicrotaskを追加した場合はどうなるのか?
  4. setTimeoutqueueMicrotaskPromise.resolve のコールバックをすべて実行したあとに呼び出されている。
    1. → Taskキューに追加されている。
    2. → Taskキューの中身の実行は、Microtaskキューの中身が空になってから行われる。
      Q2. Task内でMicrotaskを追加した場合はどうなるのか?

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

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

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

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

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

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

標準出力

queueMicrotask 1
queueMicrotask 2
Task

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

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

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

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

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

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

標準出力

setTimeout
queueMicrotask 1

Q3. で、queueMicrotaskはどんな用途があるの?

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

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

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

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"));
            });
	}
};

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

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

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

Fetching data
Data fetched
Loaded data

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

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

Fetching data
Loaded data
Data fetched

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

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

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

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

Using microtasks in JavaScript with queueMicrotask() - Web APIs | MDN

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?