ホームページ >ウェブフロントエンド >フロントエンドQ&A >JavaScriptはシーケンシャルに実行されますか?
JavaScript はシーケンシャルに実行されます。 JavaScript はシングルスレッド言語であり、実行順序はトップダウンです。つまり、コードの実行中に別のコードを実行したい場合は、現在のコードが実行されるまで待機する必要があります。完了。
このチュートリアルの動作環境: Windows7 システム、JavaScript バージョン 1.8.5、Dell G3 コンピューター。
1. シングルスレッド JavaScript
皆さんご存知のとおり, JavaScript はシングルスレッド言語であり、実行順序はトップダウンです。 JavaScript にはマルチスレッドの概念がなく、すべてのプログラムは単一のスレッドによって順次実行されます。つまり、コードの実行中に別のコードを実行したい場合は、現在のコードの実行が完了するまで待機する必要があります。
JavaScript は 1 つのスレッドでのみ実行されることに注意してください。これは、JavaScript エンジンのスレッドが 1 つだけであることを意味するわけではありません。実際、JavaScript エンジンには複数のスレッドがあります。1 つのスクリプトは 1 つのスレッド (メイン スレッドと呼ばれます) でのみ実行できます。他のスレッドはバックグラウンドで連携します。
では、なぜ js なのかマルチスレッドではなくスレッドについてはどうですか?マルチスレッドの方が効率的ではないでしょうか?
JavaScript がマルチスレッドではなくシングルスレッドである理由は、その目的に関連しており、Web スクリプト言語としての JavaScript の主な目的は、ユーザーと対話し、DOM を操作することです。
JavaScript に同時に 2 つのスレッドがあり、1 つのスレッドが Web ページの DOM ノードにコンテンツを追加し、もう 1 つのスレッドがノードを削除する場合、ブラウザはどちらのスレッドを使用する必要がありますか?ロック機構も付いているのでしょうか?
JavaScript は誕生以来シングルスレッドです。その理由は、マルチスレッドはリソースを共有する必要があり、互いの実行結果を変更する可能性があるため、ブラウザが複雑になりすぎないようにするためです。 Webスクリプト言語、これは複雑すぎます。
したがって、複雑さを避けるために、JavaScript は最初からシングルスレッドでした。これは言語の中核機能となっており、将来も変わることはありません。
このモードの利点は、実装が比較的簡単で、実行環境が比較的シンプルであることです。欠点は、1 つのタスクに時間がかかる限り、後続のタスクをキューに入れる必要があることです。これにより、プログラム全体が遅延します。
一般的なブラウザの応答不能 (サスペンドデス) は、JavaScript コードの特定の部分が長時間実行される (無限ループなど) ことが原因で発生することが多く、ページ全体がその場所でスタックし、他のタスクが実行できなくなります。実行される。
JavaScript 言語自体は遅いわけではありません。遅いのは、Ajax リクエストが結果を返すのを待つなど、外部データの読み取りと書き込みです。このとき、相手のサーバーが長時間応答しなかったり、ネットワークがスムーズでなかったりすると、スクリプトが長時間停滞してしまいます。
キューの原因が大量の計算であり、CPU がビジー状態である場合は、そのことを忘れてください。ただし、多くの場合、IO 操作 (入力および出力) が非常に遅いため (Ajax 操作など)、CPU がアイドル状態になっています。ネットワークからの読み取り)データ)、続行する前に結果が出るまで待つ必要があります。
JavaScript 言語の設計者は、現時点では、CPU が IO 操作を完全に無視し、待機中のタスクを一時停止し、後のタスクを最初に実行できることに気づきました。 IO 操作が結果を返すまで待ってから、戻って中断されたタスクの実行を続行します。この仕組みがJavaScriptの内部で使われる「イベントループ」の仕組み(Event Loop)です。
シングルスレッド モデルは JavaScript に大きな制限を課しますが、他の言語にはない利点ももたらします。うまく使えば JavaScript プログラムが詰まることはなく、そのため Node は非常に少ないリソースで大量のトラフィック アクセスを処理できます。
マルチコア CPU のコンピューティング能力を活用するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御されるため、 DOM を操作しないでください。したがって、この新しい標準は JavaScript のシングルスレッドの性質を変更しません。
ここでコードの一部を見てみましょう
function fn(){ console.log('start'); setTimeout(()=>{ console.log('setTimeout'); },0); console.log('end'); } fn() // 输出 start end setTimeout
JavaScript の実行順序はトップダウンですが、上記のコードの実行順序が乱れているのはなぜでしょうか?
#JavaScript には同期と非同期の 2 つの実行モードがあるため
2. JavaScript の同期および非同期
プログラム内のすべてのタスクは、同期タスク (同期) と 非同期タスク (非同期) の 2 つのカテゴリに分類できます。
同期と非同期とは何ですか?同期と非同期はどのように実装されますか?
同期タスク: これらは、エンジンによって一時停止されず、メイン スレッドでの実行のためにキューに入れられるタスクです。次のタスクは、前のタスクが実行された後にのみ実行できます。
异步任务: 是那些被引擎放在一边,不进入主线程、而进入 任务队列 的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。(通俗讲就是 只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。)
排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数。
js中包含诸多创建异步的函数如:
seTimeout,setInterval,dom事件,ajax,Promise,process.nextTick等函数
3.任务队列和事件循环
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。
如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。
异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
因为单线程,所以代码自上而下执行,所有代码被放到执行栈
中执行;
遇到异步函数将回调函数添加到一个任务队列
里面;
当执行栈
中的代码执行完以后,会去循环任务队列
里的函数;
将任务队列
里的函数放到执行栈
中执行;
如此往复,称为事件循环
;
JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环。
这样分析,上面那一段代码就得到了合理的解释。
再来看一下这段代码:
function fn() { setTimeout(()=>{ console.log('a'); },0); new Promise((resolve)=>{ console.log('b'); resolve(); }).then(()=>{ console.log('c') }); } fn() // b c a
4.Promise和async 立即执行
Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。
而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await等到之后做了什么?
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到微任务(microtask)中,然后就会跳出整个async函数来执行后面的代码。
不管await后面的代码是同步还是异步,await总是需要时间,从右向左执行,先执行右侧的代码,执行完后,发现有await关键字,于是让出线程,阻塞代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
例如:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); }
等同于
async function async1() { console.log('async1 start'); Promise.resolve(async2()).then(() => { console.log('async1 end'); }) }
5.宏任务和微任务
两个任务分别处于任务队列中的宏队列 和 微队列中;宏任务队列
与微任务队列
组成了任务队列;任务队列
将任务放入执行栈
中执行
宏任务
宏队列,macrotask,也叫tasks。
异步任务的回调会依次进入macro task queue,等待后续被调用。
宏任务一般包括:
微任务
微队列,microtask,也叫jobs。
异步任务的回调会依次进入micro task queue,等待后续被调用
微任务一般包括:
process.nextTick (Node独有)
Promise
Object.observe
MutationObserver
1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等)。
2.全局Script代码执行完毕后,执行栈
Stack会清空。
3.先从微任务队列
中取出位于队首的回调任务,放入执行栈
Stack中执行,执行完后微队列
长度减1。
4.继续循环取出位于微队列
的任务,放入执行栈
Stack中执行,以此类推,直到直到把微任务
执行完毕。注意,如果在执行微任务
的过程中,又产生了微任务
,那么会加入到微队列
的末尾,也会在这个周期被调用执行。
5.微队列
中的所有微任务
都执行完毕,此时微队列
为空队列,执行栈
Stack也为空。
6.取出宏队列
中的任务,放入执行栈
Stack中执行。
7.执行完毕后,执行栈
Stack为空。
8.重复第3-7个步骤。
以上是完成的****事件循环
6.面试题测试
现在我们再来分析一下最开始的那个面试题
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'); /* script start async1 start async2 promise1 script end async1 end promise2 setTimeout */
我们分析一下整个过程:
1.首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。
2.然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中。
3.script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。
遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log(‘async1 end’)加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。
4.script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
5.script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。
根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。
因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 end和promise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
6.第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。
再来一个稍微复杂点的代码
function fn(){ console.log(1); setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); },0); new Promise((resolve, reject) => { console.log(4); resolve(5); }).then(data => { console.log(data); }); setTimeout(() => { console.log(6); },0); console.log(7); } fn(); //
流程重现
1.执行函数同步语句
console.log(1);
执行栈: [ console ]
宏任务: []
微任务: []
打印结果:
1
setTimeout(() => { // 这个回调函数叫做callback1,setTimeout属于宏任务,所以放到宏队列中 console.log(2); Promise.resolve().then(() => { console.log(3) }); });
执行栈: [ setTimeout ]
宏任务: [ callback1 ]
微任务: []
打印结果:
1
new Promise((resolve, reject) => { // 注意,这里是同步执行的 console.log(4); resolve(5) }).then((data) => { // 这个回调函数叫做callback2,promise属于微任务,所以放到微队列中 console.log(data); });
执行栈: [ promise ]
宏任务: [ callback1 ]
微任务: [ callback2 ]
打印结果:
1
4
setTimeout(() => { // 这个回调函数叫做callback3,setTimeout属于宏任务,所以放到宏队列中 console.log(6); })
执行栈: [ setTimeout ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]
打印结果:
1
4
console.log(7)
执行栈: [ console ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]
打印结果:
1
4
7
2.同步语句执行完毕,从微队列
中依次取出任务执行,直到微队列
为空
console.log(data) // 这里data是Promise的成功参数为5
执行栈: [ callback2 ]
宏任务: [ callback1 , callback3 ]
微任务: []
打印结果:
1
4
7
5
3.这里微队列
中只有一个任务,执行完后开始从宏队列
中取任务执行
console.log(2);
执行栈: [ callback1 ]
宏任务: [ callback3 ]
微任务: []
打印结果:
1
4
7
5
2
但是执行callback1
的时候遇到另一个Promise,Promise异步执行完毕以后在微队列
中又注册了一个callback4
函数
Promise.resolve().then(() => { // 这个回调函数叫做callback4,promise属于微任务,所以放到微队列中 console.log(3); });
执行栈: [ Promise ]
宏任务: [ callback3 ]
微任务: [ callback4 ]
打印结果:
1
4
7
5
2
4.取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行
console.log(3)
执行栈: [ callback4 ]
宏任务: [ callback3 ]
微任务: []
打印结果:
1
4
7
5
2
3
5.微队列
全部执行完,再去宏队列
**中取第一个任务执行
console.log(6)
执行栈: [ callback3 ]
宏任务: []
微任务: []
打印结果:
1
4
7
5
2
3
6
6.以上全部执行完毕,执行栈
,宏队列
,****微队列
**均为空
执行栈: []
宏任务: []
微任务: []
打印结果:
1
4
7
5
2
3
6
总结
1、代码的检查装载阶段(预编译阶段),此阶段进行变量和函数的声明,但是不对变量进行赋值, 变量的默认值为undefined。
2、代码的执行阶段,此阶段对变量进行赋值和函数的声明。 所以:Js的变量提升和函数提升会影响JS的执行结果,ES6中的let定义的变量不会提升。
3、js的执行顺序,先同步后异步。
4、异步中任务队列的执行顺序: 先微任务microtask队列,再宏任务macrotask队列。
5、调用Promise 中的resolve,reject属于微任务队列,setTimeout等属于宏任务队列 所以:
【同步>异步;微任务>宏任务】
【相关推荐:javascript学习教程】
以上がJavaScriptはシーケンシャルに実行されますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。