前書き#
フロントエンドに入ってから 2 年以上が経ちました。以前、私はある記事で「フロントエンド分野の変化と不変を理解する必要がある」と書きました。今も 2 年前と同じように、さまざまなフレームワークが次々と登場し、目が回るようです。多くの場合、「これはまだ学ぶ必要があるのか」と自問自答しています。これが私に不安感をもたらすことがよくあります。また、新しい事物に直面したとき、私はしばしば手足をもがいてしまいます。結局のところ、基礎知識が不十分で、内面的な力が不足しているのです。これが私がこの記事を書く目的でもあります —— 内面的な力を高め、「変化と不変」をより良く理解するためです。
始めましょう#
まずはこのコードから探索の旅を始めましょう。
console.log('始まりました')
// setTimeout1
setTimeout(function () {
console.log('timeout1')
// promise1
new Promise(function (resolve) {
console.log('timeout1_promise')
resolve()
}).then(function () {
console.log('timeout1_then')
})
}, 2000)
for (var i = 1; i <= 5; i++) {
// setTimeout2
setTimeout(function () {
console.log(i)
}, i * 1000)
console.log(i)
}
// promise2
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('then1')
})
// setTimeout3
setTimeout(function () {
console.log('timeout2')
// promise3
new Promise(function (resolve) {
console.log('timeout2_promise')
resolve()
}).then(function () {
console.log('timeout2_then')
})
}, 1000)
// promise4
new Promise(function (resolve) {
console.log('promise2')
resolve()
}).then(function () {
console.log('then2')
})
始める前に、問題を考えるのが好きな仲間は 2 つの質問があるかもしれません。なぜ JavaScript はシングルスレッド言語として設計されているのか、なぜ同期タスクと非同期タスクの区別があるのか。この 2 つの質問については、ここでは詳しく述べません。興味のある方は阮先生のこの記事をご覧ください。
さて、js が大まかにどのように実行されるか見てみましょう。
一体どれがマクロタスクで、どれがマイクロタスクなのでしょうか。大まかにこのように区別します。
マクロタスク(macro-task)
- script(全体の JavaScript コード)
- setTimeout()
- setInterval()
- setImmediate()
- I/O
- UI render
マイクロタスク(micro-task)
- promise
- async/await(同☝)
- process.nextTick
- MutationObserver
予備知識はほぼ整いました。では、上記のコードを実行しましょう。
- まず全体の script がメインスレッドに入り、
console.log()
に出会い、すぐに『始まりました』と出力します。 - 次に
setTimeout
に出会い、2 秒後にコールバック関数 function () がマクロタスク Event Queue に分配されます(ここで注意すべきは、2 秒後に実行されるのではなく、分配されるということです)。ここでは setTimeout1 とマークします。 for
に出会い、直接実行します。同様に、setTimeout
内のコールバック関数がマクロタスク Event Queue に分配されます(ここではクロージャの知識が関係します)、setTimeout2 とマークし、次にconsole.log
を実行し、『1、2、3、4、5』と出力します。promise
に出会い、new Promise
が直接実行され、『promise1』と出力します。then
がマイクロタスク Event Queue に分配され、then1 とマークします。- 次にもう一つの
setTimeout
に出会い、1 秒後にコールバック関数 function () がマクロタスク Event Queue に分配され、setTimeout3 とマークします。 - 再び
promise
に出会い、同様に『promise2』と出力し、then
がマイクロタスク Event Queue に分配され、then2 とマークします。
第一ラウンドのイベントループのマクロタスクはすでに実行が完了しました。Event Queue 内のタスクは以下の表の通りです。
マクロタスク | マイクロタスク |
---|---|
setTimeout1 (2 秒後) | then1 |
setTimeout2 (1 秒後) | then2 |
setTimeout3 (1 秒後) |
上記のフローチャートに基づき、マクロタスクが完了した後、js エンジンの監視プロセスがこの時点で実行可能なマイクロタスクがあるかどうかを確認します。この時、マイクロタスク Event Queue に分配されたthen
が実行されます。順次『then1』、『then2』と出力されます。
第一ラウンドのイベントループはすべて実行完了です。
さて、次に第二ラウンドのイベントループが始まります(1 秒後)。
setTimeout2
に出会い、『6』と出力します。実行可能なマイクロタスクはありません。新しいマクロタスクを実行します。setTimeout3
に出会い、『timeout2』と出力し、new promise
が即座に実行され、『timeout2_promise』と出力します。then
がマイクロタスク Event Queue に分配され、then3 とマークします。第二ラウンドのイベントループのマクロタスクは実行完了しました。Event Queue 内のタスクは以下の表の通りです。
マクロタスク | マイクロタスク |
---|---|
setTimeout1 (1 秒後) | then3 |
setTimeout2 (1 秒後) |
同様に、マクロタスクが実行完了した後、このラウンドのマイクロタスク then3 を実行します。
第二ラウンドのイベントループはすべて実行完了です。
setTimeout1
に出会い、console.log
を実行し、『timeout1』と出力します。new promise
が即座に実行され、『timeout1_promise』と出力し、then
がマイクロタスク Event Queue に分配され、then4 とマークします。- 第三ラウンドのイベントループのマクロタスクが実行完了し、このラウンドのマイクロタスク
then
を実行し、『timeout1_then』と出力します。
第三ラウンドのイベントループは実行完了です。
setTimeout2
の中で、4 つのマクロタスクが順次生成され、1 秒ごとに 6 を出力します。
これで、全てのコードが実行完了しました。
まとめ:マクロタスクが実行完了したら、そのマクロタスクによって生成されたマイクロタスクを実行します。もしマイクロタスクの実行中に新しいマイクロタスクが生成された場合、マイクロタスクを続けて実行します。マイクロタスクが完了したら、マクロタスクに戻り、次のラウンドを開始します。