Maison > Article > interface Web > Avancement front-end (12) : Explication détaillée du mécanisme de boucle d'événements
JavaScript l'apprentissage est dispersé et compliqué, nous apprenons souvent quelque chose, mais nous ne le ressentons pas . Quand j'ai progressé, même après un certain temps, j'oublie ce que j'ai appris. Afin de résoudre mon problème, j'ai essayé de trouver un indice central pendant le processus d'apprentissage. Tant que je suis cet indice, je peux progresser petit à petit.
L'avancée de base du front-end se déploie lentement autour de cet indice, et le mécanisme EventLoop (Event Loop) est le plus points de connaissance critiques des indices. Par conséquent, j'ai étudié le mécanisme de boucle d'événements sans arrêt et j'ai résumé cet article pour le partager avec vous.
Le mécanisme de boucle d'événements dans son ensemble nous indique la séquence d'exécution du code JavaScript que nous écrivons. Cependant, au cours de mon étude, j'ai trouvé de nombreux articles de blogs nationaux qui n'en donnaient qu'une explication superficielle. De nombreux articles dessinaient un cercle sur l'image pour indiquer un cycle. Après l'avoir lu, je n'avais pas l'impression de comprendre grand-chose. . Mais il est si important que lorsque nous souhaitons passer un entretien pour des postes de niveau intermédiaire ou supérieur, le mécanisme de boucle d'événements est toujours un sujet incontournable. Surtout après que PromiseObject a été officiellement ajouté à ES6, comprendre le mécanisme de boucle d'événements dans la nouvelle norme est devenu encore plus important. C'est très embarrassant.
Il y a eu récemment deux articles populaires qui ont également exprimé l'importance de cette question.
Cet entretien frontal cause des problèmes
80 % des candidats échouent à la question de l'entretien JSMais malheureusement, les maîtres ont dit à tout le monde que ce point de connaissance est très important, mais n'a-t-il pas Je ne dirai pas à tout le monde pourquoi c’est arrivé. Ainsi, lorsque nous rencontrons une telle question lors d'un entretien, même si vous connaissez le résultat, si l'intervieweur pose d'autres questions, nous sommes toujours confus.
Avant d'apprendre le mécanisme de boucle d'événements, je suppose que vous comprenez déjà les concepts suivants. Si vous avez encore des questions, vous pouvez revenir en arrière et lire mes articles précédents.
Contexte d'exécution
File d'attenteStructure des données (file d'attente)
Promesse (Je résumerai l'utilisation détaillée de Promise dans le prochain article Avec encapsulation personnalisée)
Parce que le mécanisme de boucle d'événements dans le nouveau standard de chrome navigateur est presque le même que nodejs, il est donc intégré ici Comprenons nodejs ensemble . Nous allons présenter plusieurs API que nodejs possède mais pas dans le navigateur. Il vous suffit de la comprendre, et vous n'avez pas forcément besoin de savoir comment l'utiliser. Par exemple, processus.suivantCochez, setImmédiat
OK, puis je donnerai d'abord la conclusion, puis je démontrerai l'événement en détail avec des exemples et illustrations.
Nous savons qu'une fonctionnalité majeure de JavaScript est un fil unique, et ce fil a une boucle d'événements unique.
Bien sûr, le web worker du nouveau standard implique le multi-threading, je n'y connais pas grand-chose, donc je n'en parlerai pas ici.
Lors de l'exécution du code JavaScript, en plus de s'appuyer sur la pile d'appels de fonctions pour déterminer l'ordre d'exécution des fonctions, il s'appuie également sur la file d'attente des tâches pour exécuter d'autres codes .
Dans un fil de discussion, la boucle d'événements est unique, mais Vous pouvez avoir plusieurs files d'attente de tâches.
La file d'attente des tâches est divisée en macro-tâches (macro-tâches) et micro-tâches (micro-tâches). Dans les dernières normes, elles sont appelées respectivement tâches et travaux.
la macro-tâche comprend probablement : script (code global), setTimeout, setInterval, setImmediate, I/O, UI rendering.
la micro-tâche comprend probablement : process.nextTick, Promise, Object.observe (obsolète), MutationObserver (html5nouvelle fonctionnalité)
setTimeout/Promise, etc. sont appelés sources de tâches. Ce qui entre dans la file d'attente des tâches est la tâche d'exécution spécifique spécifiée.
// setTimeout中的回调函数才是进入任务队列的任务 setTimeout(function() { console.log('xxxx'); })
Les tâches provenant de différentes sources de tâches entreront dans différentes files d'attente de tâches. Parmi eux, setTimeout et setInterval ont la même origine.
L'ordre de la boucle d'événements détermine l'ordre d'exécution du code JavaScript. Il démarre la première boucle du script (le code global). Le contexte global entre ensuite dans la pile des appels de fonction. Jusqu'à ce que la pile d'appels soit effacée (il ne reste que la pile globale), alors toutes les micro-tâches sont exécutées. Une fois que toutes les micro-tâches exécutables ont été exécutées. La boucle recommence à partir de la macro-tâche, trouve l'une des files d'attente de tâches, puis exécute toutes les micro-tâches, et la boucle continue.
L'exécution de chaque tâche, qu'il s'agisse d'une macro-tâche ou d'une micro-tâche, est complétée à l'aide d'une pile d'appels de fonctions.
L'expression de texte pur est en effet un peu sèche, nous utilisons donc ici 2 exemples pour comprendre progressivement la séquence spécifique de la boucle événementielle.
// 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');
Tout d'abord, la boucle d'événements démarre à partir de la file d'attente des tâches de macro. À l'heure actuelle, il n'y a qu'une seule tâche de script (code global) dans la file d'attente des tâches de macro. La séquence d'exécution de chaque tâche est déterminée par la pile d'appels de fonction. Lorsqu'une source de tâche est rencontrée, la tâche sera d'abord distribuée dans la file d'attente correspondante. Par conséquent, la première étape de l'exemple ci-dessus est illustrée dans la figure ci-dessous.
Étape 2 : lorsque la tâche de script est exécutée, il rencontre d'abord setTimeout, et setTimeout est une source de tâche de macro, son rôle est alors de distribuer la tâche dans sa file d'attente correspondante.
setTimeout(function() { console.log('timeout1'); })
Étape 3 : Une instance Promise est rencontrée lors de l'exécution du script. Le premier paramètre du constructeur Promise est exécuté lorsqu'il est nouveau, il n'entrera donc dans aucune autre file d'attente, mais sera exécuté directement sur la tâche en cours, et les .then suivants seront distribués à la file d'attente Promise de micro-tâche.
Par conséquent, lorsque le constructeur est exécuté, les paramètres à l'intérieur entrent dans la pile d'appels de fonction pour l'exécution. for loop n'entrera dans aucune file d'attente, donc le code sera exécuté dans l'ordre, donc promise1 et promise2 ici seront affichés dans l'ordre.
et la tâche de script continue de s'exécuter. À la fin, une seule phrase est générée : global1, puis la. la tâche globale est terminée.
Étape 4 : Une fois le premier script de macrotâche exécuté, toutes les microtâches exécutables commencent. À l'heure actuelle, il n'y a qu'une seule tâche dans la file d'attente Promise, then1, dans la microtâche, elle peut donc être exécutée directement. Le résultat de l'exécution est alors affiché. Bien entendu, son exécution est également exécutée dans la pile d'appels de fonction.
Étape 5 : Lorsque toutes les micro-tâches sont exécutées, le premier tour de la boucle est terminé. C’est à ce moment-là que doit commencer le deuxième tour du cycle. Le deuxième cycle part toujours de la macro-tâche.
À ce moment-là, nous avons constaté que parmi les macro-tâches, il n'y avait qu'une seule tâche timeout1 en attente de être exécuté dans la file d'attente setTimeout. Alors exécutez-le directement.
À l'heure actuelle, il n'y a aucune tâche dans la file d'attente des tâches macro et la file d'attente des micro-tâches, donc le code ne sera plus affiché. Quelque chose d'autre.
Ensuite, le résultat de l'exemple ci-dessus est évident. Vous pouvez l'essayer vous-même et en faire l'expérience.
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// 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,具体如何做到的,下一篇文章我们慢慢分析。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!