一、はじめに#
まず、JavaScript
はシングルスレッドの言語であり、同時に一つのことしかできないことを意味しますが、シングルスレッドであることはブロッキングを意味しません。シングルスレッドの非ブロッキングの方法はイベントループです。
JavaScript
では、すべてのタスクを次のように分類できます。
-
同期タスク:即座に実行されるタスクで、通常はメインスレッドで直接実行されます。
-
非同期タスク:非同期に実行されるタスク、例えば
ajax
ネットワークリクエスト、setTimeout
タイマー関数など
同期タスクと非同期タスクの実行フローチャートは以下の通りです:
上記の図からわかるように、同期タスクはメインスレッド、つまりメイン実行スタックに入り、非同期タスクはタスクキューに入ります。メインスレッド内のタスクが完了すると、対応するタスクをタスクキューから読み取り、メインスレッドにプッシュします。このプロセスはイベントループと呼ばれます。
二、マクロタスクとマイクロタスク#
タスクを同期タスクと非同期タスクに分けることは必ずしも正確ではありません。例を挙げましょう:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
上記のコードをフローチャートで分析すると、次の実行手順が得られます:
console.log(1)
:同期タスクであり、メインスレッドで実行されます。setTimeout()
:非同期タスクであり、Event Table
に配置され、0 ミリ秒後にconsole.log(2)
コールバックがEvent Queue
にプッシュされます。new Promise
:同期タスクであり、メインスレッドで直接実行されます。.then
:非同期タスクであり、Event Table
に配置されます。console.log(3)
:同期タスクであり、メインスレッドで実行されます。
したがって、分析によると、結果は1
=> 'new Promise'
=> 3
=> 2
=> 'then'
となるはずです。
しかし、実際の結果は1
=> 'new Promise'
=> 3
=> 'then'
=> 2
となります。
この分岐の原因は非同期タスクの実行順序です。イベントキューは実際には「先入れ先出し」のデータ構造であり、前にあるイベントがメインスレッドによって優先的に読み取られます。
例では、setTimeout
のコールバックイベントが最初にキューに入るべきですが、.then
のコールバックイベントが先にキューに入ってしまいます。
その理由は、非同期タスクはマイクロタスクとマクロタスクにさらに分類できるからです。
マイクロタスク#
非同期に実行する関数であり、メイン関数の実行が終了した後、現在のマクロタスクが終了する前に実行されます。
一般的なマイクロタスクには次のものがあります:
-
Promise.then
-
MutaionObserver
-
Object.observe(非推奨;Proxy オブジェクトに置き換え)
-
process.nextTick(Node.js)
マクロタスク#
マクロタスクは比較的大きなタイムスパンで実行され、実行間隔を正確に制御することはできません。リアルタイム性の高い要件にはあまり適していません。
一般的なマクロタスクには次のものがあります:
- script(外部同期コードとして理解できます)
- setTimeout/setInterval
- UI レンダリング / UI イベント
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
この時点で、イベントループ、マクロタスク、マイクロタスクの関係は次の図のようになります。
このフローに従って、実行メカニズムは次のようになります。
- マクロタスクを実行し、マイクロタスクがあればマイクロタスクのイベントキューに入れます。
- 現在のマクロタスクが完了したら、マイクロタスクのイベントキューをチェックし、すべてのマイクロタスクを順番に実行します。
上記の問題に戻りましょう。
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
フローは次のようになります。
// console.log(1)に遭遇し、1を直接出力します
// タイマーに遭遇し、後で実行するためにマクロタスクに入れます
// new Promiseに遭遇し、これは直接実行され、'new Promise'を出力します
// .thenはマイクロタスクであり、マイクロタスクキューに入れます
// console.log(3)に遭遇し、3を直接出力します
// これで、現在のマクロタスクが完了しました。今度はマイクロタスクリストをチェックして、.thenのコールバックを実行し、'then'を出力します
// 一度マクロタスクが完了したら、新しいマクロタスクを実行します。ここではタイマーのマクロタスクのみが残っていますので、それを実行し、2を出力します
三、async と await#
async
は非同期を意味し、await
はasync wait
と解釈できます。つまり、async
は非同期メソッドを宣言するために使用され、await
は非同期メソッドの実行を待機するために使用されます。
async#
async
関数はpromise
オブジェクトを返します。次の 2 つの方法は同等です。
function f() {
return Promise.resolve('TEST');
}
// asyncFはfと同等です!
async function asyncF() {
return 'TEST';
}
await#
通常、await
の後にはPromise
オブジェクトがあり、そのオブジェクトの結果を返します。Promise
オブジェクトでない場合は、その値を直接返します。
async function f(){
// 同じ意味です
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
await
の後に続くものに関係なく、await
は後続のコードをブロックします。
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // ブロック
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上記の例では、await
は下のコードをブロックします(つまり、マイクロタスクキューに追加します)。同期コードを実行してから、ブロックされたコードを実行します。
したがって、上記の出力結果は1
、fn2
、3
、2
です。
四、フロー分析#
上記の理解に基づいて、JavaScript のさまざまなシナリオの実行順序を大まかに把握することができます。
以下にコードを示します:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
分析プロセス:
- コード全体を実行し、
console.log('script start')
に遭遇し、結果を直接出力します。出力はscript start
です。 - タイマーに遭遇し、マクロタスクとして保留します。
async1()
に遭遇し、async1
関数を実行し、console.log('async1 start')
を出力します。次に、await
に遭遇し、どうすればよいか?まず、async2
を実行し、console.log('async2')
を出力します。次に、下のコードをブロックします(つまり、マイクロタスクリストに追加します)。同期コードの実行を中断し、同期コードの実行が完了したら、ブロックされたコードを再度実行します。new Promise
に移動し、直接実行し、console.log('promise1')
を出力します。次に、.then()
に遭遇し、マイクロタスクとしてマイクロタスクリストに入れます。- 最後の行を直接出力し、
console.log('script end')
を出力します。これで同期コードの実行が完了し、マイクロタスクを実行します。つまり、await
の下のコードを実行し、console.log('async1 end')
を出力します。 - 次に、次のマイクロタスクを実行します。つまり、
.then()
のコールバックを実行し、console.log('promise2')
を出力します。 - 前のマクロタスクが完了したので、次のマクロタスクを実行します。これはタイマーのマクロタスクのみですので、それを実行し、
console.log('settimeout')
を出力します。
したがって、最終的な結果は次のとおりです:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout