はじめに#
フロントエンドに入ってからもう 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 はシングルスレッド言語と設計されたのか、そしてなぜ同期タスクと非同期タスクが区別されるのか。これらの問題については、ここでは詳しく説明しません。興味のある方は、阮一峰先生のこの記事をご覧ください。
さて、JavaScript の実行方法をざっくりと見てみましょう。
どのタスクがマクロタスクであり、どのタスクがマイクロタスクなのかを具体的に見てみましょう。
マクロタスク(macro-task)
- script (JavaScript の全体的なコード)
- setTimeout()
- setInterval()
- setImmediate()
- I/O
- UI のレンダリング
マイクロタスク(micro-task)
- promise
- async/await (同じく)
- process.nextTick
- MutationObserver
準備がほぼ整いましたので、上記のコードを実行してみましょう。
- まず、全体のスクリプトがメインスレッドに入り、
console.log()
に遭遇し、すぐに『はじめました』と出力されます。 - 次に、
setTimeout
に遭遇し、2 秒後にコールバック関数 function () がマクロタスクイベントキューにディスパッチされます(ここで 2 秒後に実行されるわけではありません)。これを setTimeout1 とマークします。 for
に遭遇し、直接実行され、同様にsetTimeout
のコールバック関数がマクロタスクイベントキューにディスパッチされます(ここではクロージャの知識が関係しています)、これを setTimeout2 とマークし、次にconsole.log
を実行して『1、2、3、4、5』と出力します。promise
に遭遇し、new Promise
が直ちに実行され、『promise1』が出力されます。then
はマイクロタスクイベントキューにディスパッチされ、then1 とマークされます。- 次に、
setTimeout
に再び遭遇し、1 秒後にコールバック関数 function () がマクロタスクイベントキューにディスパッチされ、これを setTimeout3 とマークします。 - また
promise
に遭遇し、同様に実行され、『promise2』が出力され、then
がマイクロタスクイベントキューにディスパッチされ、then2 とマークされます。
最初のイベントループのマクロタスクがすべて実行されました。イベントキューのタスクは以下の表のようになります。
マクロタスク | マイクロタスク |
---|---|
setTimeout1 (2 秒後) | then1 |
setTimeout2 (1 秒後) | then2 |
setTimeout3 (1 秒後) |
上記のフローチャートに従って、マクロタスクが実行された後、JavaScript エンジンの監視プロセスは実行可能なマイクロタスクがあるかどうかをチェックします。この時、マイクロタスクイベントキューにディスパッチされたthen
が実行されます。順番に『then1』、『then2』と出力されます。
最初のイベントループが完了しました。
さて、第 2 のイベントループが始まります(1 秒後)。
setTimeout2
に遭遇し、『6』を出力し、実行可能なマイクロタスクはありません。新しいマクロタスクを実行します。setTimeout3
に遭遇し、『timeout2』を出力し、new promise
が直ちに実行され、『timeout2_promise』が出力されます。then
はマイクロタスクイベントキューにディスパッチされ、then3 とマークされます。
第 2 のイベントループのマクロタスクがすべて実行されました。イベントキューのタスクは以下の表のようになります。
マクロタスク | マイクロタスク |
---|---|
setTimeout1 (1 秒後) | then3 |
setTimeout2 (1 秒後) |
同様に、マクロタスクが実行された後、このイテレーションのマイクロタスク then3 が実行されます。
第 2 のイベントループが完了しました。
setTimeout1
に遭遇し、console.log
を実行し、『timeout1』を出力します。new promise
が直ちに実行され、『timeout1_promise』が出力され、then
はマイクロタスクイベントキューにディスパッチされ、then4 とマークされます。- 第 3 のイベントループのマクロタスクが実行され、このイテレーションのマイクロタスク
then
が実行され、『timeout1_then』が出力されます。
第 3 のイベントループが完了しました。
setTimeout2
に遭遇し、『6』を出力します。実行可能なマイクロタスクはありません。新しいマクロタスクを実行します。
以上で、コード全体の実行が終了しました。
要約すると、マクロタスクが実行され、そのマクロタスクが生成したマイクロタスクが実行されます。マイクロタスクの実行中に新しいマイクロタスクが生成された場合は、引き続きマイクロタスクが実行されます。マイクロタスクの実行が完了したら、マクロタスクに戻り、次のイテレーションを開始します。