blog

細かい非同期のアレコレ

前置き\n非同期関数 obj;\n await b...

Oct 27, 2020 · 5 min. read
シェア

序文

 //  
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')
}, 0);
async1();
new Promise (function(resolve) {
 console.log('promise1');
 resolve()
}).then(function () {
 console.log('promise2');
});
console.log('script end'); 

このコードはJSの非同期を総合的に検討したもので、イベントループ、マクロタスク/マイクロタスクなどの知識がないと、この問題の正解が書けないかもしれません。少なくとも私は既に失神しているのを見たことがあります。大丈夫です、浅いところから深いところまで、コード実行の過程で丁寧に口うるさく言いながら、実は優先順位の分け方を教えてくれています。

JSの実行方法

まず知っておかなければならないのは、JSはシングルスレッドであり、一般論として、一度に一つのことしかできません。そのため、setTimeout待ちイベントなどのイベントが発生した場合、実際にそのタイマーがカウントを終了し、実行をダウンさせるために待つことができない、これはあまりにも非効率的です。これはあまりにも非同期をもたらします。例えば

console.log('1'); 
setTimeout(()=>{
 console.log('2')
}, 0);
console.log('3');
// 1
// 3
// 2

この遅延を0にしても、まず3が実行され、次に2が実行されることがわかります。では、上記のsetTimeoutの他に、非同期処理のトリガーとなるコードには何があるでしょうか?setInterval、Ajax、promise、async/awaitがあります!

では、非同期はどのように実装されるのでしょうか?非同期はコールバックに基づいて実装され、イベントループは非同期コールバックを実装するための基本原理です。

イベントループ

前述したように、JSはシングルスレッドなので、メインスレッドと呼ばれるコードがステップごとに実行されます。メインスレッドが非同期処理に遭遇すると、その非同期タスクをタスクキューに格納し、メインスレッドの実行を続けます。メインスレッドが実行を終了すると、イベントループを通してタスクキューを要求し、非同期コードを実行します。

イベントループはイベントポーリング機構とも呼ばれ、実際にはメインスレッドがタスクキューにコールバックを要求し続けるための機構です。キューに実行可能な非同期タスクが照会られると、タスクはコールスタックにプッシュされ、実行されます。

Dom操作は非同期ではありませんが、イベント・ループに基づくコールバックでもあることは特筆に値します。

マクロ・タスクとマイクロ・タスク

マクロタスクとマイクロタスクについて説明しながら、例を見てみましょう。

console.log('1'); 
setTimeout(()=>{
 console.log('2')
}, 0);
Promise.resolve().then(()=> {
 console.log('3')
})
console.log('4');
// プリントの順番は1、4、3だ。2

面白いですね!非同期は順番に実行されるはずですが、上の例では3が2の前に出力されているのがわかります。ここで、マイクロタスクとマクロタスクの概念と、それらの実行順序について説明します。

ここでまずいくつかの結論が得られます

  • マクロタスクには、Ajax、Dom操作、setTimeout/setIntervalがあります。
  • マイクロタスクには、promise、async/awaitが含まれます。
  • マイクロタスクはマクロタスクよりも早く実行されます。

最初の2点は覚えておいて損はありません。3点目の理由については、詳しく話したり、コードで示したりします。

<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="UTF-8">
 </head>
 <body>
 <div id="container"></div>
 <script src="https://..////../.js"></>
 <script src="1.js"></script>
 </body>
</html>
const $tag = $('<p>1</p>');
$('#container').append($tag);
console.log($('#container').children().length) // domノードを追加するコードを同期させた
// alertのブロックメカニズムを使って、レンダリングするかどうかを判断する。
Promise.resolve().then(()=> {
 console.log('マイクロタスクを行う場合length', $('#container').children().length) // 1
 alert('promise')
});
setTimeout(() => {
 console.log('が入力されたらマクロタスクを実行する。length', $('#container').children().length) // 1
 alert('settimeout')
}, 0);

プロミスがトリガーされたとき、同期コードでコンテナに新しいノードが追加されているにもかかわらず、domはまだレンダリングされていないことが簡単にわかります。

OK をクリックした後、setTimeout を実行すると、正常にレンダリングされます。
まとめると、マイクロタスクとマクロタスクの基本的な違いは実行タイミングにあり、マイクロタスクはdomのレンダリングの前に実行され、マクロタスクはその逆です。

一般的な考え方を書きます。まず、非同期関数の定義は、同期コードの最初の実行に従って、実行されず、その後、非同期コードの原則の実装は、最初のプリントアウトは、スクリプトの開始であり、その後、マクロタスクであるsettimeoutに、最初に入れ、その後、async1()関数の実装は、async1の開始を印刷し、その後、async2をトリガし(awaitの次の文は、プロミス内のthenと等価であるため、async2を印刷するので、実際には非同期コールバックに属し、ここでasync1終了が実行されませんが、最初に一時的にタスクキューに格納され、プロミスの初期化は、最初の関数のパラメータの直接実装である同期コードと等価である、つまり、プリントのPromise1は、その後の内容も非同期であり、タスクキューに格納され、この時間の同期コードの実行が完了すると、非同期部分の実装の開始です。最初の非同期タスクは、async1終了を印刷し、その後、プロミスthenでpromise2を印刷し、最後にマクロタスク、setTimeoutを実行することです。

まとめ

JSの実行手順は以下の通りです。

1, 同期コードの最初の行実行

2、非同期が発生し、最初に一時的にタスクキューに格納されます。

3は、同期実行が完了すると、タスクキュータスクの実行を開始します。

4、マイクロタスクの実行

5、ドムレンダリング

6、マクロタスクの実行

このステップさえ覚えておけば、非同期に関するどんなコードも簡単に書けると思います。

Ps.今回初めて技術系の記事を書きましたが、非常に面白く、元々他の人の記事を読んで理解した気になっていたのですが、実際に書いてみると、記事の構成を考えたり、分かりやすく伝えるにはどうしたらいいかなど、ブレインストーミングのような作業でもあることが分かりました。インプットからアウトプットまでが本当に大変だと実感しました。 もし間違いがあれば、訂正してください、議論することを歓迎します!

なかなか書けないので、転載が必要な方はご連絡ください!

Read next

集約プロジェクトを作成する

mybatis-generator-for-imocプロジェクトを使って逆誕生させます。このプロジェクトをインポートした後、まずIDEAでmavenの設定を行います。宣言アノテーションの@-->@-->...

Oct 27, 2020 · 3 min read