ホームページ > 記事 > ウェブフロントエンド > フロントエンドの進化(12):イベントループの仕組みを詳しく解説
JavaScript学習は分散していて複雑なので、何かを学んだものの進歩を感じられず、学んだことをすぐに忘れてしまうことさえあります。この問題を解決するために、私は学習の過程で核となるヒントを見つけようと努めてきました。このヒントに従っていけば、少しずつ進歩することができます。
フロントエンドの基本的な進歩は、この手がかりを中心にゆっくりと展開されており、EventLoopメカニズム (イベントループ) は、この手がかりの最も重要な知識ポイントです。そこで、私はイベント ループの仕組みを徹底的に研究し続け、この記事にまとめて皆さんと共有しました。
イベント ループ メカニズム全体は、作成した JavaScript コードの実行順序を示します。しかし、勉強していくうちに、表面的に説明しているだけの国内ブログ記事が多く、読んだ後はあまり理解できなかったように感じました。 。しかし、これは非常に重要であるため、中堅から上級レベルのポジションの面接を行う場合、イベント ループのメカニズムは常に避けては通れないトピックになります。特に Promiseオブジェクトが ES6 に正式に追加されてからは、新しい標準のイベント ループ メカニズムを理解することがさらに重要になっています。これはとても恥ずかしいことです。
最近、この問題の重要性を表明する人気のある記事が 2 つあります。
このフロントエンド面接は問題を引き起こしています
80%の候補者がJS面接の質問に不合格ですしかし、残念なことに、専門家はこの知識ポイントが非常に重要であると皆に言いましたが、なぜそうなるのかを全員に伝えていませんでした。そのため、面接中にそのような質問に遭遇したとき、結果はわかっていても、面接官がさらに質問をすると、やはり混乱してしまいます。
イベント ループのメカニズムを学ぶ前に、次の概念をすでに理解していると思います。まだ質問がある場合は、前の記事に戻って読んでください。 E 実行コンテキスト
omeブラウザの新標準におけるイベントループの仕組みはnodejsとほぼ同じなので、ここではこれから紹介するnodejsを統合することで理解していきますAPIはいくつか利用可能です理解する必要があるだけで、必ずしもその使用方法を知る必要はありません。例えば、process.nextTick、setImmediateOK, それでは先に結論を述べてから、例と図を使ってイベントループの仕組みを詳しく説明します。
JavaScript の主な特徴はシングルスレッドであり、このスレッドにはイベント ループが 1 つだけあることです。
JavaScript コードの実行中、関数呼び出しスタックに依存して関数の実行順序を決定するだけでなく、タスク キューにも依存して他のコードの実行を決定します。
キューのデータ構造
タスクキューはマクロタスク(マクロタスク)とマイクロタスク(マイクロタスク)に分かれており、最新の規格ではそれぞれタスク、ジョブと呼ばれています。
マクロタスクには、大まかに、スクリプト (コード全体)、set
TimesetTimeout/Promiseなどをタスクソースと呼びます。タスクキューに入るのは、指定された特定の実行タスクです。
// setTimeout中的回调函数才是进入任务队列的任务 setTimeout(function() { console.log('xxxx'); })
異なるタスクソースからのタスクは、異なるタスクキューに入ります。このうち、setTimeoutとsetIntervalは由来が同じです。
イベントループの順序によって、JavaScript コードの実行順序が決まります。これにより、スクリプト (コード全体) から最初のループが開始されます。次に、グローバル コンテキストが関数呼び出しスタックに入ります。コール スタックがクリアされるまで (グローバル スタックのみが残るまで)、すべてのマイクロタスクが実行されます。すべての実行可能なマイクロタスクが実行された後。ループは再びマクロタスクから開始され、タスクキューの 1 つを見つけてからすべてのマイクロタスクを実行し、ループが継続します。
各タスクの実行は、マクロタスクであってもマイクロタスクであっても、関数呼び出しスタックの助けを借りて完了します。
純粋なテキスト表現は確かに少し無味乾燥なので、ここでは 2 つの例を使用して、イベント ループの具体的なシーケンスを徐々に理解します。
// demo01 出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。 // 为了方便理解,我以打印出来的字符作为当前的任务名称 setTimeout(function() { console.log('timeout1'); }) new Promise(function(resolve) { console.log('promise1'); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log('promise2'); }).then(function() { console.log('then1'); }) console.log('global1');
まず、マクロタスクキューからイベントループが開始されます。この時点で、マクロタスクキューにはスクリプト(全体コード)タスクが1つだけあります。各タスクの実行順序は関数呼び出しスタックによって決定され、タスク ソースに遭遇すると、タスクは最初に対応するキューに分配されます。そのため、上記の例の最初のステップが次の図に示されています。
ステップ 2: スクリプト タスクが実行されると、最初に setTimeout がマクロ タスク ソースであるため、その役割が発生します。タスクをキュー内の対応するタスクに分散することです。
setTimeout(function() { console.log('timeout1'); })
ステップ 3: スクリプトの実行中に Promise インスタンスが発生します。 Promise コンストラクター の最初のパラメーターは新しいときに実行されるため、他のキューには入りませんが、現在のタスクで直接実行され、後続の .then はタスクの Promise キューに micro-Go に配布されます。
したがって、コンストラクターが実行されると、内部のパラメーターが実行のために関数呼び出しスタックに入ります。 forループはキューに入らないので順番にコードが実行されるので、ここのpromise1とpromise2が順番に出力されます。
resolveがスタックにプッシュされて実行されます。コンストラクターが実行され、完了後に解決が実行され、スタック、promise2 が出力され、promise1 ページがスタックから飛び出し、then が実行されると、promise タスク then1 が対応するキューに入ります
ステップ 4: 最初のマクロタスク スクリプトが実行された後、すべての実行可能なマイクロタスクの実行を開始します。このとき、マイクロタスクには Promise キューに then1 のタスクが 1 つしかないので、実行結果は then1 に出力されます。もちろん、その実行も関数呼び出しスタックで実行されます。
すべてのマイクロタスクを実行する
ステップ 5: すべてのマイクロタスクが実行されると、サイクルの最初のラウンドが終了したことを意味します。この時点で、サイクルの 2 番目のラウンドを開始する必要があります。 2 番目のサイクルは依然としてマクロタスクから始まります。
この時点で、マクロタスクのうち、setTimeoutキューで実行を待っているtimeout1タスクが1つだけであることがわかりました。したがって、直接実行してください。
この時点では、マクロタスクキューとマイクロタスクキューにタスクがないため、コードはそれ以外に何も出力しません。
すると、上記の例の出力は明らかです。自分で試して体験することができます。
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// demo02 console.log('golb1'); setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) }) setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) }) process.nextTick(function() { console.log('glob1_nextTick'); }) new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') }) setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) }) process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') }) setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。
第一步:宏任务script首先执行。全局入栈。glob1输出。
第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。
setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) })
第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。
setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) })
第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。
process.nextTick(function() { console.log('glob1_nextTick'); })
第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。
new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') })
第六步:执行遇到第二个setTimeout。
setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) })
第七步:先后遇到nextTick与Promise
process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') })
第八步:再次遇到setImmediate。
setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。
其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。
当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。
这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。
setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。
大家需要注意这里的循环结束的时间节点。
当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。
OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。
当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。
// 用数组模拟一个队列 var tasks = []; // 模拟一个事件分发器 var addFn1 = function(task) { tasks.push(task); } // 执行所有的任务 var flush = function() { tasks.map(function(task) { task(); }) } // 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中 setTimeout(function() { flush(); }) // 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法 var dispatch = function(name) { tasks.map(function(item) { if(item.name == name) { item.handler(); } }) } // 当然,我们把任务丢进去的时候,多保存一个name即可。 // 这时候,task的格式就如下 demoTask = { name: 'demo', handler: function() {} } // 于是,一个订阅-通知的设计模式就这样轻松的被实现了
这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。
因此,在老的浏览器没有支持Promise的时候,就可以利用setTimeout等方法,来模拟实现Promise,具体如何做到的,下一篇文章我们慢慢分析。
以上がフロントエンドの進化(12):イベントループの仕組みを詳しく解説の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。