JavaScriptの仕組み-Event Loopをご紹介します

6308 ワード

1.jsは単一スレッドです.
まず、jsは単一スレッドであることがよく知られていますが、なぜこのような低効率な実行方法は依然として淘汰されていませんか.これはその用途によって決まる.jsの主な用途はユーザーのインタラクションとDOM操作で、例えばjsが同時に2つのスレッドがあって、1つのスレッドがあるDOMノードに内容を追加して、もう1つのスレッドがこのノードを削除して、この時ブラウザは戸惑って、どのスレッドを標準にしますか?(実行性能を向上させるため、新しいhtml 5には、メインスレッドにサブスレッドを追加できるweb workerが追加されていますが、DOMを操作できないことが制限されています.)
2.タスクキュー(task queue)
jsは単一スレッドであるため、タスクの実行にはキューが必要であり、1つずつ実行され、前のタスクが終了してから、次のタスクが開始されます.しかし、1つのタスクが非同期タスクである場合、ブラウザは、その戻り結果が実行され続けるまで長い時間待つ必要があり、中間待機時間cpuは空きです.jsの対応策は,このタスクを一時的に棚上げし,他のタスクを実行することである.戻り結果がある場合は、タスクを再実行します.
これはしばらく放置され、どこに放置されているのか、答えはタスクキューです.
同期タスクとは、プライマリ・スレッド上で実行されるタスクで、前のタスクが完了した場合にのみ、次のタスクが実行されます.非同期タスクとは、プライマリ・スレッドに入らずにタスク・キューに入るタスクで、プライマリ・スレッド・タスクの実行が完了した場合にのみ、タスク・キューのタスクがプライマリ・スレッドの実行に入ります.
3.実行スタック(JS stack)
まず,スタック(heap)とスタック(stack)の概念を理解する.スタックは静的にメモリを割り当てるために使用され、スタックは動的にメモリを割り当てるために使用され、コンピュータメモリに存在します.スタックは先に出て、スタックは先に出ています.jsのすべてのタスクはjs実行スタックで実行されます.先にスタックに入ったタスクは実行されますが、js実行スタック内には1つのタスクしかありません.(後述)
4.マクロタスクとマイクロタスク(task&Microstask)
前述したように、非同期タスクはプライマリスレッド上で実行されませんが、非同期タスクだけでなく、すべてのマイクロタスクはプライマリスレッド上で実行されません.これにより、上記のタスクキューをマイクロタスクキューと呼ぶことができます.マクロタスクはメインスレッド上で直接実行され、マイクロタスクはタスクキューに入り、実行を待つ必要があります.
コード(example 1)を見てみましょう
console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

この出力結果は何ですか?
順序は次のとおりです.
script start
script end
promise1
promise2
setTimeout

まず、セグメントコード全体をscriptラベルとして、マクロタスクとしてjs実行スタックに直接アクセスして実行します.
出力script start;
settimeoutに遭遇し、0秒後にsettimeoutは独立したマクロタスクとしてマクロタスクキューに追加されます.(ここでは、マクロタスクキュー、すなわち、上述したプライマリスレッドについて説明する)
promiseに遭遇すると、promiseが完了した最初のthenは独立したマイクロタスクとして「マイクロタスクキュー」に追加され、2番目のthenはまたマイクロタスクとしてマイクロタスクのキューに追加されます.
次にscript endを出力します.
次に、scriptはマクロタスク全体の実行が完了し、js実行スタックが空で、マクロタスクキュー(プライマリスレッド)にsettimeoutがあり、マイクロタスクキューにはpromise(then)タスクが2つあります.どちらを先に実行しますか?私たちが前に言った非同期タスク実行ポリシーを思い出すと、次のjs実行スタックに入るのが最初のpromise(then)であることは推測に難くない.
出力promise 1;
次に、マクロタスクキューとマイクロタスクキューを見ます.マイクロタスクキューにはpromise(then)があるので、このマイクロタスクをjs実行スタックに押し込んで実行します.
出力promise 2;
このとき、マイクロタスクキューは空なので、マクロタスクキュー内のタスクを実行し、settimeout;
出力settimeout;
要約すると、タスクはマクロタスクとマイクロタスクに分けられ、マクロタスクキュー(プライマリスレッド)とマイクロタスクキューに対応します.マイクロタスクは、現在実行中のスクリプトが終了した直後に実行されるタスクです.1つのタスクの実行が終了すると、js実行スタックが空になります.この場合、まずマイクロタスクキューにタスクを探しに行きます.マイクロタスクキューが空でない場合、1つのマイクロタスクをjs実行スタックに追加します.現在のマイクロタスクキューが空の場合、マクロタスクキュー内のタスクを実行します.
マイクロタスクとマクロタスクを区別する方法:
マクロタスク(task):時間順にスタックを圧縮して実行するので、ブラウザはJavaScript内部タスクとDOMタスクを秩序正しく実行することができます.1つのtask実行が終了すると、次のtask実行が開始される前に、ブラウザはページを再レンダリングできます.各taskは、ユーザのクリック操作からクリックイベント、HTMLドキュメントのレンダリング、および上記の例のsettimeoutなど、割り当てる必要があります.
settimeoutの動作原理は、遅延が完全に正確ではないことを知っていると信じています.これは、settimeoutが遅延時間の終了後にevent loopに新しいtaskを割り当てるので、すぐに実行するのではなく、settimeoutのコールバック関数が前のtaskが完了してから実行されるのを待つからです.これは、「settimeout」が「script end」の後に出力される理由です.「script end」は最初のtaskの一部であり、「settimeout」は新しいtaskであるからです.
マイクロタスク(Microstask):通常、一連のタスクに応答する必要があるか、新しいtaskを割り当てる必要がなく、非同期のタスクを実行する必要があるなど、現在のtaskの実行が終了した後にすぐに実行する必要があるタスクです.これにより、パフォーマンスのオーバーヘッドを削減できます.microtaskタスクキューはtaskタスクキューとは独立したキューであり、microtaskタスクは各taskタスクの実行が終了した後に実行されます.各taskで生成されたmicrotaskはmicrotaskキューに追加され、microtaskで生成されたmicrotaskは現在のキューの末尾に追加され、microtaskはキュー内のすべてのタスクを順番に処理します.Microtaskタイプのタスクには、現在、MutationObserverおよびPromiseのコールバック関数が含まれています.
Promiseが決定されるたびに(または拒否されるたびに)、microtaskタスクキューに新しいmicrotaskとしてコールバック関数が追加されます.これにより、Promiseが非同期で実行できることも保証されます.呼び出すとthen(resolve,reject)の場合、上の「promise 1」と「promise 2」が「script end」の後に出力される理由は、microtaskタスクキューのタスクが現在のtask実行が終了してから実行される必要があるため、すぐに新しいmicrotaskがキューに追加されます.一方、「promise 1」および「promise 2」の出力は、「settimeout」より前である.これは、「settimeout」が新しいtaskであり、microtaskが現在のtaskが終了した後、次のtaskが開始する前に実行されるためである.
ステップアップ版、task&Microstask(example 2):

    
var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);

当我们点击inner这个div的时候会输出什么那?

顺序是:

click
promise
mutate
click
promise
mutate
timeout
timeout

どうしてそうなの?
ここでは、このinnerのclickに対応するリスニング関数が実行されると、タスクの完了とみなされ、マイクロタスクキュー内のpromise(then)とmutationObserverのコールバックが実行されるclick操作をマクロタスクとして説明する.この2つのタスクの実行が完了すると、マイクロタスクキューが空になり、バブルによるouterのclickが実行されます.outerのclickタスクとマイクロタスクが実行されると、マクロタスクキュー(プライマリスレッド)に残っている2つのsettimeoutのタスクが検索されます.実行スタックに1つずつ押し込みます.
スーパーステップ版(example 3):
上のjsコードに次の行のコードを加えると、何か違いがありますか?
inner.click()

答えは次のとおりです.
click
click
promise
mutate
promise
timeout
timeout

どうしてこんなに违うの?次に詳しく分析します.
前の例では、2つのマイクロタスクが2つのclickの間で実行されたが、この例では、2つのclickの後に実行された.
まずclick()がトリガしたイベントはタスクとして実行スタックに圧入され、それによって生成されたinnerの傍受関数はまたタスクとして実行スタックに圧入され、このコールバック関数によって生成されたタスクが実行された後、clickが出力され、マイクロタスクキューにpromiseとmutateが追加されるので、上記のようにpromiseとmutateを実行すべきではないでしょうか.ただし、jsがスタック内のinnerを実行するためではない.click()はまだ実行が終わっていないのでinner.click()のイベントはouterの傍受関数をトリガし、これによりclickが出力され、このコールバックが終了するとinner.click()このタスクが終了すると、マイクロタスクキュー内のタスクが実行されます.
簡単に言えば、この例ではinnerを呼び出すためです.click()は、イベントリスナーのコールバック関数が非同期ではなく現在実行されているスクリプトと同期して実行されるようにするので、現在のスクリプトの実行スタックはJS実行スタックに圧縮されます.したがって、この例のマイクロタスクは、各clickイベントの後に実行されるのではなく、2つのclickイベントの実行が完了した後に実行される.
5.Event Loop
js実行スタックは、メインスレッドおよびマイクロタスクキューからタスクを読み取り、実行し続けるので、この実行メカニズム全体をEvent Loop(イベントループ)と呼ぶ.
注意:本明細書のすべての実行結果はchromeブラウザ、その他のブラウザ、または出入りに基づいています.
参考記事:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/