この記事では、JavaScript イベント ループを理解する方法を説明します。 (写真とテキスト)、それは確かな参考価値があるので、困っている友人が参考になれば幸いです。
コンピューターのメモリ内のデータにアクセスする 基本的なデータ構造はスタックとキューに分かれています。
スタックは後入れ先出しのデータ構造です。スタックは「スタック」と呼ばれることもありますが、「ヒープ」はスタックとはまったく異なる別の複雑なデータ構造であることに注意してください。スタックの特徴は、操作が一方の端でのみ実行されることです。一般に、スタック操作はプッシュとポップの 2 つだけです。スタックにプッシュされた最初のデータは常に最後に出力されます。
キューはスタックに似ていますが、データの挿入操作はキューの一方の端から実行され、削除操作はキューの端から実行されます。違った終わり方。
よくある比喩として、スタックは直立したバケツのようなもので、最初にスタックに入れられたデータは、スタックから取り出されるときに、そのデータがバケツの一番下に置かれます。バケットの口で 1 つずつ取り出されるので、スタック上のデータが常に最後に取り出されます。キューは水道管のようなもので、キューに入れられた最初のデータがキューの反対側から最初に流出します。これが両者の最大の違いです。
JavaScript では、関数の実行は典型的なプッシュおよびポップのプロセスです:
function fun1() { function fun2() { function fun3() { console.log('do it'); } fun3(); } fun2(); } fun1();
プログラムが実行されると、最初に fun1、fun2、および fun3 が順番にスタックにプッシュされ、関数が呼び出されるときに fun3 が呼び出されます。最初に Call (pop) がプッシュされ、次に fun2 と fun1 がプッシュされます。想像してみてください。fun1 が最初にスタックからポップされた場合、関数 fun2 と fun3 は失われます。
JavaScript 言語では、プログラムはシングルスレッドであり、メインスレッドが 1 つしかありません。これはなぜでしょうか。なぜなら、JavaScript がもともとブラウザ内で実行されるスクリプト言語として設計されたことは想像に難くありません。JavaScript がマルチスレッドになるように設計されており、2 つのスレッドが同時に DOM を変更する場合、誰の決定が使用されるのでしょうか。したがって、JavaScript はシングルスレッドであり、プログラムが途中でさらに時間のかかる操作がある場合は、コードが 1 文ずつ実行されます。
シングルスレッド設計のため、言語の実行効率が非常に低くなります。マルチコア CPU のパフォーマンスを活用するために、JavaScript 言語は、より時間のかかる操作がある場合、非同期コードをサポートします。非同期タスクがまだ実行されていない場合、メインスレッドは非同期タスクを一時停止し、後続の同期コードの実行を継続します。走行を終了しました。
このコードの実行方法は、実際には私たちの生活の多くのシーンと非常に一致しています。たとえば、シャオミンは仕事から帰ってきて、喉が渇いていて、お茶を飲むために水を沸かしたいと考えています。水が沸騰していないときは、シャオミンがバカのように待っていると、お茶を作る前に水が沸騰するのを待ちます。テレビを見たり音楽を聴いたりして、水が沸騰するのを待ちます。それからお茶を淹れに行きます。明らかに、2 番目の非同期メソッドの方が効率的です。
一般的な非同期操作とは何ですか?たくさんありますが、一般的なものをいくつか挙げることができます:
Ajax
DOMイベント操作
setTimeout
Promiseのthenメソッド
ノードの読み取りファイル
撮ってみましょうまずコードを見てください:
//示例1 console.log(1); setTimeout(function () { console.log(2); }, 1000); console.log(3);
このコードは非常に単純です。これらをブラウザに入力し、次のように結果を実行します:
1 3 2
setTimeout 関数は実行を 1000 ミリ秒遅らせるため、1 と 3 が最初に出力されます。 2 は 1000 ミリ秒後に出力するのが非常に論理的です。
コードを少し変更して、setTimeout の遅延時間を 0 に変更してみましょう:
//示例2 console.log(1); setTimeout(function () { console.log(2); }, 0); //0毫秒,不延时 console.log(3);
実行結果:
1 3 2
最後の出力 2 が 0 ミリ秒の遅延のままなのはなぜですか?心配しないで、コードの一部を見てみましょう:
//示例3 console.log(1); setTimeout(function () { console.log(2); }, 0); Promise.resolve().then(function(){ console.log(3); }); console.log(4);
実行結果:
1 4 3 2
上記の 3 つのコードについて、結果を正しく記述でき、このように出力される理由を説明できれば、次のようになります。 JavaScript のイベント ループを理解していることは明らかです。理解できない場合は、ここで何が起こったのかを話しましょう。実際、非常に興味深いです。
最初に基本的なデータ構造について簡単に説明しますが、これは今話しているイベント ループと何か関係がありますか?もちろん、それはあります。最初に明確にしておきたいのは、JavaScript コードはすべてスタック上で実行されるということです。
一般に、コードは同期コードと非同期コードに分類できます。実際、非同期コードは、マクロ タスクとマイクロ タスクの 2 つのカテゴリにさらに分類できます。
マクロ タスクとミクロ タスクが何であるかについては心配しないでください。多くの場合、このような高レベルの用語は理解に役立たないことがよくあります。まず、マクロとはマクロを意味し、ミクロとはミクロと小さいことを意味します。
JavaScript はインタプリタ言語であり、その実行プロセスは次のとおりです:
从上到下依次解释每一条js语句
若是同步任务,则压入一个栈(主线程);如果是异步任务,就放到一个任务队列里
开始执行栈里的同步任务,直到将栈里的所有任务都走完,此时栈清空了
回过头看异步队列里如果有异步任务完成了,就生成一个事件并注册回调,压入栈中
再返回第3步,直到异步队列都清空,程序运行结束
语言描述的费劲,不如看图:
通过以上的步骤可以看到,不论是同步还是异步,只要是执行的时候都是要在栈里执行的,而一遍又一遍的回头检查异步队列,这种执行方式 就是所谓的“事件环”。
明白了javascript的执行原理,我们就不难理解之前的第二段代码,为什么setTimeout为0时会最后执行,因为setTimeout是异步代码,必须要等所有的同步代码都执行完,才会执行异步队列。即使setTimeout执行得再快,它也不可能在同步代码之前执行。
聊了这么多,我们好像还没有说宏任务和微任务的话题呢,上面说了,异步任务又分为微任务和宏任务,那它们又是一个怎样的执行机制呢?
注意!微任务和宏任务的执行方式在浏览器和Node中有差异,有差异!重要的事我们多说几遍,以下我们讨论的是在浏览器的环境里。
在浏览器的执行环境中,总是先执行小的、微任务,再执行大的、宏任务,回过头再看看第三段代码,为什么Promise的then方法在setTimeout之前执行?其根本原理就是因为Promise的then方法是一个微任务,而setTimeout是一个宏任务。
接下来我们借用阮一峰老师的一张图来说明:
其实,以上这张图示我们可以再将它细化一点,这个图上的异步队列只画了一个,也就是说没有区分微任务队列和宏任务队列。我们可以脑补一下,在此图上多加一个微任务队列,当javascript执行时再多加一个判断,如果是微任务就加到微任务队列里,宏任务就加到宏任务队列里,在清空队列时,浏览器总会优先清空“微任务”。这样就把浏览器的事件环撤底说全了。
最后来一个大考,以下代码的运行结果是什么:
<script> setTimeout(function () { console.log(1); Promise.resolve().then(function () { console.log(2); }); }); setTimeout(function () { console.log(3); }); Promise.resolve().then(function () { console.log(4); }); console.log(5); </script>
将此代码拷到chrome中跑一下,结果是:
5 4 1 2 3
不妨我们试着分析一下为什么是这个结果,首先输出5,因为console.log(5)
是同步代码,这没什么可说的。
之后将前两个setTimeout和最后一个Promise放入异步队列,注意它们的区分,此时执行完了同步代码之后发现微任务和宏任务队列中都有代码,按浏览器的事件环机制,优先执行微任务,此时输出4。
然后执行宏任务队列里的第一个setTimeout,输出1。
此时,setTimeout中又有一个Promise,放入微任务队列。
再次清空微任务队列,输出2。
最后宏任务队列里还有最后一个setTimeout,输出3。
而Node中的事件环又和浏览器有些许的不同,在node.js的官方文档中有专门的描述,其中文档中有一张图,详细的说明了它的事件环机制,我们把它拿出来:
可以看到,node.js中的事件环机制分为了6个阶段,其中最重要的3个阶段我在上面做了注明:
timer阶段,指的就是setTimeout等宏任务
poll轮询阶段,如读取文件等宏任务
check阶段,setImmediate宏任务
图中每一个阶段都代表了一个宏任务队列,在Node事件环中,微任务的运行时机是在每一个“宏任务队列”清空之后,在进入下一个宏任务队列之间执行。这是和浏览器的最大区别。
还是用代码说话吧,有一道经典的Node.js事件环面试题:
const fs = require('fs'); fs.readFile('./1.txt', (err, data) => { setTimeout(() => { console.log('timeout'); }); setImmediate(() => { console.log('immediate'); }); Promise.resolve().then(() => { console.log('Promise'); }); });
运行结果:
Promise immediate timeout
代码并不复杂,首先使用fs模块读取了一个文件,在回调的内部有两个宏任务和一个微任务,微任务总是优于宏任务执行的,因此先输出Promise。
但是之后的区别为什么先输出immdiate?原因就在于fs读取文件的宏任务在上图中的第4个轮询阶段,当第4个阶段清空队列之后,就该进入第5个check阶段,也就是setImmediate这个宏任务所在的阶段,而不会跳回第1个阶段,因此先输出immedate。
最后总结一下,分析完浏览器和Node的事件环发现它们并不简单,但只要记住了它们之间的区别就可以分析出结果。
浏览器事件环是运行完一个宏任务马上清空微任务队列。
Node事件环是清空完一个阶段的宏任务队列之后再清空微任务队列。
最后,总结一下常见的宏任务和微任务:
宏任务 | 微任务 |
---|---|
setTimeout | Promise的then方法 |
setInterval | process.nextTick |
setImmediate | MutationObserver |
MessageChannel |
相关推荐:
以上がJavascript のイベント ループを理解するにはどうすればよいですか? (写真とテキスト)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。