概要
以前
でMacrotasksとMicrotasksについて触れました。
ただこの時使ったのがPromise
、setTmeout()
だけで、他の種類の非同期関数を使った場合どうなるのだろう、と気になり検証してみました。
環境
- Node.js v8.9.0
各キューの種類と優先度
図
以下の図が非常に分かりやすいです。
ref: Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3
Event Loop(Macrotasks)のキュー
大きく以下の3つを覚えてください
種類 | 実行順 |
---|---|
Timer(setTimeout, setInterval) | 1 |
I/O event | 2 |
setImmediate | 3 |
Microtasksのキュー
大きく以下の2つを覚えてください
種類 | 実行順 |
---|---|
process.nextTick | 1 |
Other(Promise etc...) | 2 |
Event LoopとMicrotasksの順序
Event Loopの各キューはMicrotasksのキューが空になってから実行されます。
検証
Macrotasksのキューのみのケース
基本
以下のコードの場合
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => console.log('setImmediate')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs')); setTimeout(() => console.log('setTimeout'), 0); console.log('script end');
結果
以下の通りの結果になります。
script start script end setTimeout fs setImmediate
先程の実行順の通りですね。
応用1
複数キューに詰め込む場合
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => console.log('setImmediate1')); setImmediate(() => console.log('setImmediate2')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs1')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs2')); setTimeout(() => console.log('setTimeout1'), 0); setTimeout(() => console.log('setTimeout2'), 0); console.log('script end');
結果
複数でも優先度は変わりません
script start script end setTimeout1 setTimeout2 fs1 fs2 setImmediate1 setImmediate2
応用2
後からキューに詰め込む場合
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => { console.log('setImmediate') setTimeout(() => console.log('nested setTimeout'), 0); }); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs')); setTimeout(() => console.log('setTimeout'), 0); console.log('script end');
結果
Timerのキューの方が先と言っても、後からキューに登録する場合は次のループで実行されるので当然setImmediate()
より後になります。
script start script end setTimeout fs setImmediate nested setTimeout
Microtasksのキューのみ(ほぼ)のケース
基本
console.log('script start'); Promise.resolve().then(() => console.log('Promise')); process.nextTick(() => console.log('nextTick')); console.log('script end');
結果
優先度のとおりです。
script start script end nextTick Promise
応用1
複数のケース
console.log('script start'); Promise.resolve().then(() => console.log('Promise1')); Promise.resolve().then(() => console.log('Promise2')); process.nextTick(() => console.log('nextTick1')); process.nextTick(() => console.log('nextTick2')); console.log('script end');
結果
複数でも優先度は変わりません。
script start script end nextTick1 nextTick2 Promise1 Promise2
応用2
ネストするケース。
console.log('script start'); Promise.resolve() .then(() => console.log('Promise')) .then(() => { process.nextTick(() => console.log('nested nextTick')); }); process.nextTick(() => console.log('nextTick')); console.log('script end');
結果
Promiseの実行後にnextTickがキューに詰め込まれるので、実行も後になります。
script start script end nextTick Promise nested nextTick
両方のケース
基本
先程の2つの基本形を混ぜました
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => console.log('setImmediate')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs')); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve().then(() => console.log('Promise')); process.nextTick(() => console.log('nextTick')); console.log('script end');
結果
script start script end nextTick Promise setTimeout fs setImmediate
応用1
Promiseの中でnextTickを呼んだ場合どうなるでしょう
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => console.log('setImmediate')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs')); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve() .then(() => console.log('Promise')) .then(() => { process.nextTick(() => console.log('nested nextTick')); }); process.nextTick(() => console.log('nextTick')); console.log('script end');
結果
Promise実行中にnextTick queueが増えたので、まだMicrotasksのキューが空になりません。
なのでnested nextTick
が実行されてからTimerが実行されます。
script start script end nextTick Promise nested nextTick setTimeout fs setImmediate
応用2
setTimeoutの中でMicrotasks系の非同期関数を呼んだ場合どうなるでしょう
const fs = require('fs'); const path = require('path'); console.log('script start'); setImmediate(() => console.log('setImmediate')); fs.lstat(path.join(__dirname, 'test.js'), () => console.log('fs')); setTimeout(() => { console.log('setTimeout1') Promise.resolve().then(() => console.log('nested Promise')); process.nextTick(() => console.log('nested nextTick')); }, 0); setTimeout(() => console.log('setTimeout2'), 0); Promise.resolve().then(() => console.log('Promise')); process.nextTick(() => console.log('nextTick')); console.log('script end');
結果
setTimeoutの後でnested nextTick
が実行されています。
これはEvent Loopの各キューの合間にMicrotasksが空になるまで実行されるためです。
注意としてTimerはキューが無くなるまではMicrotasksより先に実行されます
script start script end nextTick Promise setTimeout1 setTimeout2 nested nextTick nested Promise fs setImmediate
応用
上記を理解していると、次のコードも理解できます。
ref: Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3
結果
next tick1 next tick2 next tick3 promise1 resolved promise2 resolved promise3 resolved promise4 resolved promise5 resolved next tick inside promise resolve handler set timeout set immediate1 set immediate2 set immediate3 set immediate4
まとめ
非同期関数の実行順についてまとめました。