JavaScript の実行メカニズムを理解したい場合は、JavaScript のシングルスレッドメカニズム、タスクキュー (同期タスクと非同期タスク)、イベントとコールバック関数、タイマー、イベントループなどのいくつかの点を深く理解する必要があります。
JavaScript (この言語の中核でもある) の言語機能の 1 つは、シングルスレッド です。簡単に言うと、単一のスレッドは一度に 1 つのことしか実行できません。複数のタスクがある場合、それらは次のタスクを実行する前に 1 つの順序でのみ完了できます。 JavaScript のシングルスレッドは、その言語の目的に関連しています。ブラウザーのスクリプト言語としての JavaScript の主な目的は、ユーザーとの対話を完了し、DOM を操作することです。これにより、シングルスレッドのみが可能であることが決まります。それ以外の場合は、複雑な同期の問題が発生します。
JavaScript に同時に 2 つのスレッドがあると想像してください。1 つのスレッドは特定の DOM ノードにコンテンツを追加する必要があり、もう 1 つのスレッドの操作はそのノードを削除することです。では、ブラウザは誰を操作すればよいでしょうか。そのため、複雑さを避けるために、JavaScript は誕生以来シングルスレッドでした。CPU 使用率を改善するために、HTML5 は Web Worker 標準を提案しています。これにより、JavaScript スクリプトは複数のスレッドを作成できますが、子スレッドはメインスレッドによって完全に制御され、DOM を操作してはなりません。したがって、この標準は JavaScript のシングルスレッドの性質を変えるものではありません。
タスクキュー
通常、キューイングには次の 2 つの理由があります:
このことから、JavaScript の設計者は、現時点では、作業効率を向上させるために後で準備ができているタスクを実行すること、つまり待機中のタスクを一時停止して脇に置き、後で実行することが完全に可能であることにも気づきました。必要なものを手に入れること。これは、電話に出たときに相手が少し席を離れたときに別の電話がかかってきたため、現在の通話を切り、その通話が終わるのを待ってから、前の通話に再度接続するようなものです。 そこで、同期と非同期という概念が登場し、タスクは同期タスク(Synchronous)と非同期タスク(Asynchronous)の2種類に分けられました。
具体的には、非同期実行は次のとおりです。
実行スタック
(実行コンテキスト スタック) を形成します。
(2) メインスレッドの他に「タスクキュー」もあります。非同期タスクに実行結果がある限り、イベントは「タスク キュー」に配置されます。つまり、各非同期タスクは、準備が完了すると固有のフラグを設定します。このフラグは、対応する非同期タスクを識別するために使用されます。
(3) 「実行スタック」内のすべての同期タスクが実行されると、システムは「タスクキュー」を読み取り、その中にどのようなイベントがあるかを確認します。これらの対応する非同期タスクは待機状態を終了し、実行スタックに入って実行を開始します。つまり、メインスレッドは前のタスクを完了した後、「タスクキュー」内のフラグを調べて、対応する非同期タスクを実行用にパッケージ化します。
(4) メインスレッドは上記の 3 つのステップを繰り返し続けます。メインスレッドが空である限り、「タスクキュー」を読み取ります。このプロセスが何度も繰り返されます。これが JavaScript の仕組みです。
では、メインスレッドの実行スタックが空であることはどうやってわかるのでしょうか? js エンジンには監視プロセスがあり、メインスレッドの実行スタックが空かどうかを継続的にチェックし、空になるとイベント キューに移動して、呼び出されるのを待っている関数があるかどうかを確認します。 以下では、
同期タスクと非同期タスクはそれぞれ異なる実行「場所」に入り、同期的にメインスレッドに入り、非同期的にイベントテーブルに入って関数を登録します。
指定されたものが完了すると、イベントテーブルはこの関数をイベントキューに移動します。
メインスレッドのタスクが実行されると、タスクはイベントキューに移動して、対応する関数を読み取り、実行のためにメインスレッドに入ります。
上記のプロセスは継続的に繰り返され、通常はイベント ループと呼ばれます。
「タスクキュー」はイベントのキューです(メッセージのキューとしても理解できます) IO デバイスがタスクを完了すると、「タスク」に 1 つ追加されます。 「queue」イベント。関連する非同期タスクが「実行スタック」に入ることができることを示します。次に、メインスレッドは「タスクキュー」を読み取り、そこにどのようなイベントがあるかを確認します。
「タスクキュー」のイベントには、IO デバイスイベントに加えて、ユーザーが生成したイベント (マウスクリック、ページスクロールなど) も含まれます。コールバック関数が指定されている限り、これらのイベントは発生時に「タスクキュー」に入り、メインスレッドによる読み取りを待ちます。
いわゆる「コールバック関数」(コールバック)は、メインスレッドによってハングアップされるコードです。非同期タスクでは、コールバック関数を指定する必要があります。メインスレッドが非同期タスクの実行を開始すると、対応するコールバック関数が実行されます。
「タスクキュー」は先入れ先出しのデータ構造で、最初にランク付けされたイベントがメインスレッドによって最初に読み取られます。メインスレッドの読み取りプロセスは基本的に自動的に行われ、実行スタックがクリアされるとすぐに、「タスクキュー」の最初のイベントが自動的にメインスレッドに入ります。ただし、「タイマー」が含まれている場合、メインスレッドは最初に実行時間を確認する必要があります。特定のイベントは、指定された時間が経過した後にのみメインスレッドに戻ることができます。
メインスレッドは「タスクキュー」からイベントを読み込みます。このプロセスは周期的であるため、全体の動作メカニズムは「イベントループ」(イベントループ)とも呼ばれます。
イベント ループをより深く理解するために、フィリップ ロバーツのスピーチにある写真を参照してみましょう。
上の図では、メインスレッドの実行中に、ヒープ (ヒープ) とスタック (スタック) が生成され、スタック内のコードがさまざまな外部 API を呼び出し、さまざまなイベント (クリック) を「」に追加します。タスクキュー」、ロード、完了)。スタック内のコードが実行されると、メインスレッドは「タスクキュー」を読み取り、それらのイベントに対応するコールバック関数を順番に実行します。
実行スタック(同期タスク)内のコードは、常に「タスクキュー」(非同期タスク)を読み取る前に実行されます。
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } })console.log('代码执行结束');
上記は単純な ajax
リクエスト コードです。 ajax
请求代码:
ajax进入Event Table,注册回调函数success
。
执行console.log('代码执行结束')
。
ajax事件完成,回调函数success
进入Event Queue。
主线程从Event Queue读取回调函数success
并执行。
除了放置异步任务的事件,“任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做定时器(timer)功能,也就是定时执行的代码。
SetTimeout()
和setInterval()
可以用来注册在指定时间之后单次或重复调用的函数,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者会在指定毫秒数的间隔里重复调用:
setInterval(updateClock, 60000); //60秒调用一次updateClock()
因为它们都是客户端JavaScript中重要的全局函数,所以定义为Window对象的方法。
但作为通用函数,其实不会对窗口做什么事情。
Window对象的setTImeout()
方法用来实现一个函数在指定的毫秒数之后运行。所以它接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。 setTimeout()
和setInterval()
返回一个值,这个值可以传递给clearTimeout()
用于取消这个函数的执行。
console.log(1); setTimeout(function(){console.log(2);}, 1000);console.log(3);
上面代码的执行结果是1,3,2,因为setTimeout()
将第二行推迟到1000毫秒之后执行。
如果将setTimeout()
的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。
setTimeout(function(){console.log(1);}, 0);console.log(2)
上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会执行“任务队列”中的回调函数。
总之,setTimeout(fn,0)
success
を登録します。 🎜🎜🎜🎜console.log('コード実行終了')
を実行します。 🎜🎜🎜🎜ajax イベントが完了すると、コールバック関数 success
がイベント キューに入ります。 🎜🎜🎜🎜メインスレッドは、イベントキューからコールバック関数 success
を読み取り、実行します。 🎜🎜🎜🎜タイマー🎜🎜 非同期タスクのイベントを配置することに加えて、「タスク キュー」は時間指定イベント、つまり特定のコードが実行されるまでの時間を指定することもできます。これはタイマー関数と呼ばれ、定期的に実行されるコードです。 🎜🎜SetTimeout()
と setInterval()
は、1 回または指定された時間後に繰り返し呼び出される関数を登録するために使用できます。それらの内部動作メカニズムはまったく同じです。違いは、前者は指定されたコードが 1 回実行され、後者は指定されたミリ秒数の間隔で繰り返し呼び出されることです:🎜process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0)// 1// 2// TIMEOUT FIRED
🎜しかし、一般的な関数として、実際にはウィンドウに対して何も行いません。 🎜🎜 Window オブジェクトの
setTImeout()
メソッドは、指定されたミリ秒数の後に実行される関数を実装するために使用されます。したがって、2 つのパラメータを受け入れます。1 つ目はコールバック関数で、2 つ目は実行を延期するミリ秒数です。 setTimeout()
と setInterval()
は、この関数の実行をキャンセルするために clearTimeout()
に渡すことができる値を返します。 🎜setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0);
setTimeout()
が 2 行目の実行を 1000 ミリ秒後まで遅らせるためです。 🎜🎜setTimeout()
の 2 番目のパラメータが 0 に設定されている場合、現在のコードが実行された後 (実行スタックがクリアされた後)、指定されたコールバック関数がすぐに実行されます (0 ミリ秒)。間隔)。 🎜setImmediate(function (){setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
setTimeout(fn,0)
の意味は、メインスレッドの利用可能な最も早いアイドル時間に実行されるタスク、つまり、できるだけ早く実行されるタスクを指定することです。 。 「タスクキュー」の最後にイベントを追加するため、同期タスクと「タスクキュー」内の既存のイベントが処理されるまで実行されません。 🎜HTML5标准规定了
setTimeout()
的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
需要注意的是,setTimeout()
只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()
指定的时间执行。
由于历史原因,setTimeout()
和setInterval()
的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval()
)。
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对”任务队列”的理解。
process.nextTick方法可以在当前”执行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子
process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0)// 1// 2// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行。
现在,再看setImmediate。
setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0);
上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。
令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。
我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!
process.nextTick(function foo() {process.nextTick(foo); });
事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。
另外,由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。
除了广义的同步任务和异步任务,任务还有更精细的定义:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
事件循环,宏任务,微任务的关系如图所示:
按照宏任务和微任务这种分类方式,JS的执行机制是
执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
请看下面的例子:
setTimeout(function(){ console.log('定时器开始啦') }); new Promise(function(resolve){ console.log('马上执行for循环啦'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); } }).then(function(){ console.log('执行then函数啦') }); console.log('代码执行结束');
首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印”马上执行for循环啦”
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 “代码执行结束”
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印”执行then函数啦”
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印”定时器开始啦”
所以最后的执行顺序是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】
我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) })process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log
,输出1。
遇到setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。
遇到process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。
遇到Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。
又遇到了setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
* 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
我们发现了process1
和then1
两个微任务。
执行process1
,输出6。
执行then1
,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
* 第二轮事件循环宏任务结束,我们发现有process2
和then2
两个微任务可以执行。
* 输出3。
* 输出5。
* 第二轮事件循环结束,第二轮输出2,4,3,5。
* 第三轮事件循环开始,此时只剩setTimeout2了,执行。
* 直接输出9。
* 将process.nextTick()
分发到微任务Event Queue中。记为process3
。
* 直接执行new Promise
,输出11。
* 将then
分发到微任务Event Queue中,记为then3
。
宏任务Event Queue | 微任务Event Queue |
---|---|
process3 | |
then3 |
* 第三轮事件循环宏任务执行结束,执行两个微任务process3
和then3
。
* 输出10。
* 输出12。
* 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
以上がjsの実行機構例を詳しく解説の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。