ホームページ >ウェブフロントエンド >フロントエンドQ&A >JavaScriptはシーケンシャルに実行されますか?

JavaScriptはシーケンシャルに実行されますか?

青灯夜游
青灯夜游オリジナル
2022-02-09 14:35:252609ブラウズ

JavaScript はシーケンシャルに実行されます。 JavaScript はシングルスレッド言語であり、実行順序はトップダウンです。つまり、コードの実行中に別のコードを実行したい場合は、現在のコードが実行されるまで待機する必要があります。完了。

JavaScriptはシーケンシャルに実行されますか?

このチュートリアルの動作環境: Windows7 システム、JavaScript バージョン 1.8.5、Dell G3 コンピューター。

JavaScript の逐次実行実行メカニズム

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はシーケンシャルに実行されますか?

  • 因为单线程,所以代码自上而下执行,所有代码被放到执行栈中执行;

  • 遇到异步函数将回调函数添加到一个任务队列里面;

  • 执行栈中的代码执行完以后,会去循环任务队列里的函数;

  • 任务队列里的函数放到执行栈中执行;

  • 如此往复,称为事件循环;

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,等待后续被调用。

宏任务一般包括:

  1. 整体代码script
  2. setTimeout
  3. setInterval
  4. setImmediate (Node独有)
  5. requestAnimationFrame (浏览器独有)
  6. I/O
  7. UI rendering (浏览器独有)

微任务

微队列,microtask,也叫jobs。
异步任务的回调会依次进入micro task queue,等待后续被调用

微任务一般包括:

  • process.nextTick (Node独有)

  • Promise

  • Object.observe

  • MutationObserver



JavaScriptはシーケンシャルに実行されますか?

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.执行函数同步语句

  • step1
console.log(1);

执行栈: [ console ]
宏任务: []
微任务: []

打印结果:
1

  • step2
setTimeout(() => {
    // 这个回调函数叫做callback1,setTimeout属于宏任务,所以放到宏队列中
    console.log(2);
    Promise.resolve().then(() => {
        console.log(3)
    });
});

执行栈: [ setTimeout ]
宏任务: [ callback1 ]
微任务: []

打印结果:
1

  • step3
new Promise((resolve, reject) => {
    // 注意,这里是同步执行的
    console.log(4);
    resolve(5)
}).then((data) => {
    // 这个回调函数叫做callback2,promise属于微任务,所以放到微队列中
    console.log(data);
});

执行栈: [ promise ]
宏任务: [ callback1 ]
微任务: [ callback2 ]

打印结果:
1
4

  • step4
setTimeout(() => {
    // 这个回调函数叫做callback3,setTimeout属于宏任务,所以放到宏队列中
    console.log(6);
})

执行栈: [ setTimeout ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]

打印结果:
1
4

  • step5
console.log(7)

执行栈: [ console ]
宏任务: [ callback1 , callback3 ]
微任务: [ callback2 ]

打印结果:
1
4
7

2.同步语句执行完毕,从微队列中依次取出任务执行,直到微队列为空

  • step6
console.log(data)       // 这里data是Promise的成功参数为5

执行栈: [ callback2 ]
宏任务: [ callback1 , callback3 ]
微任务: []

打印结果:
1
4
7
5

3.这里微队列中只有一个任务,执行完后开始从宏队列中取任务执行

  • step7
console.log(2);

执行栈: [ callback1 ]
宏任务: [ callback3 ]
微任务: []

打印结果:
1
4
7
5
2

但是执行callback1的时候遇到另一个Promise,Promise异步执行完毕以后在微队列中又注册了一个callback4函数

  • step8
Promise.resolve().then(() => {
    // 这个回调函数叫做callback4,promise属于微任务,所以放到微队列中
    console.log(3);
});

执行栈: [ Promise ]
宏任务: [ callback3 ]
微任务: [ callback4 ]

打印结果:
1
4
7
5
2

4.取出一个宏任务macrotask执行完毕,然后再去微任务队列microtask queue中依次取出执行

  • step9
console.log(3)

执行栈: [ callback4 ]
宏任务: [ callback3 ]
微任务: []

打印结果:
1
4
7
5
2
3

5.微队列全部执行完,再去宏队列**中取第一个任务执行

  • step10
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 サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。