JavaScriptにおけるイベントループコンテキストにおけるマイクロタスクとマクロタスクの違い
JavaScript エンジンは、イベントループと呼ばれる仕組みを使用して非同期処理を管理します。イベントループは、タスクをキューに登録し、順番に処理していくループです。タスクには、マクロタスクとマイクロタスクの2種類があります。
マクロタスク
マクロタスクは、通常の JavaScript コードやタイマー、setTimeout() などの非同期APIで作成されるタスクです。イベントループが実行されると、まずマクロタスクキューからタスクを取り出して実行します。
マイクロタスクは、Promise.then() や process.nextTick() などのAPIで作成されるタスクです。イベントループがマクロタスクを実行する前に、マイクロタスクキューにあるすべてのマイクロタスクを実行します。
違い
マクロタスクとマイクロタスクの主な違いは以下の通りです。
- 実行タイミング: マイクロタスクはマクロタスクよりも先に実行されます。
- キュー: マイクロタスクは専用のキューに登録されますが、マクロタスクはタスクキューに登録されます。
- 作成方法: マクロタスクは通常の JavaScript コードで作成できますが、マイクロタスクは特定のAPIでしか作成できません。
例
以下のコード例では、マクロタスクとマイクロタスクの動作の違いを確認できます。
console.log('1'); // マクロタスク
Promise.resolve().then(() => {
console.log('2'); // マイクロタスク
setTimeout(() => {
console.log('3'); // マクロタスク
});
});
console.log('4'); // マクロタスク
このコードを実行すると、以下の順序でログが出力されます。
1
2
4
3
1と4はマクロタスクなので、イベントループが実行されると順番に処理されます。2はマイクロタスクなので、1が処理された後に実行されます。3はsetTimeout()で作成されたマクロタスクなので、2が処理された後に実行されます。
マイクロタスクとマクロタスクは、イベントループ内で処理されるタスクの2種類です。マイクロタスクはマクロタスクよりも先に実行されるため、Promiseチェーンの処理順序などを制御するのに役立ちます。
- Node.js では、イベントループに加えて、
libuv
と呼ばれるライブラリを使用して非同期処理を管理しています。libuv
は、I/Oやタイマーなどの処理を効率的に処理するために使用されます。 - マイクロタスクとマクロタスクの処理順序は、JavaScript エンジンやブラウザによって多少異なる場合があります。
console.log('1'); // マクロタスク
Promise.resolve().then(() => {
console.log('2'); // マイクロタスク
setTimeout(() => {
console.log('3'); // マクロタスク
}, 0);
});
console.log('4'); // マクロタスク
1
2
4
3
解説
- 最初に
console.log('1')
が実行されます。これはマクロタスクなので、イベントループが実行されるとすぐに処理されます。 - 次に
Promise.resolve().then(() => { ... })
が実行されます。これはPromiseを作成して、そのthen()メソッドにコールバック関数を渡すコードです。Promiseはすぐに解決されるので、then()メソッドのコールバック関数はすぐに実行されます。 - then()メソッドのコールバック関数内で、まず
console.log('2')
が実行されます。これはマイクロタスクなので、マクロタスクであるconsole.log('1')
の処理が終わった後に実行されます。 - 次に
setTimeout(() => { ... }, 0)
が実行されます。これはタイマーを設定するコードで、0ミリ秒後にコールバック関数が実行されるように指定しています。タイマーはマクロタスクなので、マイクロタスクであるconsole.log('2')
の処理が終わった後に実行されます。 - 最後に
console.log('4')
が実行されます。これはマクロタスクなので、タイマーのコールバック関数であるconsole.log('3')
の処理が終わった後に実行されます。
ポイント
- マイクロタスクはマクロタスクよりも先に実行されるので、
console.log('2')
がconsole.log('1')
よりも先に実行されます。 - setTimeout() で作成されたタイマーはマクロタスクなので、
console.log('3')
はconsole.log('2')
の後に実行されます。 - 0ミリ秒のタイマーを使用して、できるだけ早くコールバック関数を実行するようにしています。これは、マイクロタスクとマクロタスクの処理順序を明確にするためです。
async/awaitは、Promiseをより簡単に扱えるようにする構文です。async/awaitを使用してコードを書くと、以下のようにマイクロタスクとマクロタスクの処理順序を制御することができます。
(async () => {
console.log('1'); // マクロタスク
await Promise.resolve();
console.log('2'); // マイクロタスク
setTimeout(() => {
console.log('3'); // マクロタスク
}, 0);
console.log('4'); // マクロタスク
})();
このコードは、前述の例のコードと同じように動作します。
setImmediate()
setImmediate()は、Node.js 0.11以降で使用できるAPIで、次のイベントループイテレーションでマクロタスクを実行するスケジュールを設定します。setImmediate()を使用して、以下のようにマイクロタスクとマクロタスクの処理順序を制御することができます。
console.log('1'); // マクロタスク
Promise.resolve().then(() => {
console.log('2'); // マイクロタスク
setImmediate(() => {
console.log('3'); // マクロタスク
});
});
console.log('4'); // マクロタスク
このコードでは、console.log('3')
が Promise.resolve().then()
のコールバック関数内で setImmediate()
を使用してスケジュールされているため、console.log('2')
の後に実行されます。
process.nextTick()
console.log('1'); // マクロタスク
Promise.resolve().then(() => {
console.log('2'); // マイクロタスク
process.nextTick(() => {
console.log('3'); // マクロタスク
});
});
console.log('4'); // マクロタスク
注意事項
- 上記で紹介した方法は、いずれも状況に応じて使い分ける必要があります。
上記以外にも、MessageChannelやMutationObserverなどのAPIを使用して、マイクロタスクとマクロタスクの処理順序を制御することができます。
また、Web WorkerやService Workerなどのマルチスレッド環境では、タスクの処理順序がさらに複雑になる場合があります。
javascript node.js promise