Maison  >  Article  >  interface Web  >  Parlons des principes et des exemples de boucle d'événements JavaScript

Parlons des principes et des exemples de boucle d'événements JavaScript

WBOY
WBOYavant
2022-11-10 17:27:492162parcourir

Cet article vous apporte des connaissances pertinentes sur JavaScript, qui présente principalement le contenu pertinent de la boucle d'événements. Examinons-le ensemble, j'espère qu'il sera utile à tout le monde.

Parlons des principes et des exemples de boucle d'événements JavaScript

【Recommandations associées : Tutoriel vidéo JavaScript, front-end Web

La compréhension de la boucle d'événements JavaScript s'accompagne souvent de macro-tâches et de micro-tâches, d'un processus d'exécution JavaScript monothread et d'un navigateur Le mécanisme asynchrone et d'autres problèmes connexes, ainsi que les implémentations de boucles d'événements dans les navigateurs et NodeJS, sont également très différents. Être familier avec la boucle d'événements et comprendre le mécanisme de fonctionnement du navigateur nous sera très utile pour comprendre le processus d'exécution de JavaScript et résoudre les problèmes d'exécution du code.

Le principe de l'exécution asynchrone du navigateur JS

JS est monothread, c'est-à-dire qu'il ne peut faire qu'une seule chose à la fois, alors réfléchissez-y : pourquoi le navigateur peut-il exécuter des tâches asynchrones en même temps ?

Le navigateur étant multithread, lorsque JS doit effectuer une tâche asynchrone, le navigateur démarre un autre thread pour effectuer la tâche. En d'autres termes, « JS est monothread » signifie qu'il n'y a qu'un seul thread qui exécute le code JS, qui est le thread du moteur JS (thread principal) fourni par le navigateur. Il existe également des threads de minuterie et des threads de requête HTTP dans le navigateur. Ces threads ne sont pas principalement utilisés pour exécuter du code JS.

Par exemple, si vous devez envoyer une requête AJAX dans le thread principal, cette tâche sera confiée à un autre thread du navigateur (thread de requête HTTP) pour envoyer réellement la requête. Lorsque la requête reviendra, le rappel JS dont vous avez besoin. à exécuter dans le rappel sera transmis au thread JS Engine pour être exécuté. **C'est-à-dire que le navigateur est celui qui effectue réellement la tâche d'envoi de la demande, et JS est uniquement responsable de l'exécution du traitement de rappel final. ** L'asynchrone ici n'est donc pas implémenté par JS lui-même, mais constitue en fait la capacité fournie par le navigateur.

Prenons Chrome comme exemple. Le navigateur a non seulement plusieurs threads, mais également plusieurs processus, tels que le processus de rendu, le processus GPU, le processus de plug-in, etc. Chaque page à onglet est un processus de rendu indépendant, donc si un onglet plante anormalement, les autres onglets ne seront fondamentalement pas affectés. En tant que développeur front-end, vous vous concentrez principalement sur le processus de rendu. Le processus de rendu comprend les threads du moteur JS, les threads de requête HTTP et les threads de minuterie, etc. Ces threads fournissent la base à JS pour effectuer des tâches asynchrones dans le navigateur.

Une brève analyse des tâches événementielles

Derrière le principe d'exécution des tâches asynchrones du navigateur se cache en fait un ensemble de mécanismes événementiels. Le déclenchement d'événements, la sélection de tâches et l'exécution de tâches sont tous accomplis par des mécanismes pilotés par les événements. La conception de NodeJS et des navigateurs est basée sur les événements. En bref, des tâches spécifiques sont déclenchées par des événements spécifiques. Les événements ici peuvent être déclenchés par des opérations utilisateur, tels que des événements de clic, ils peuvent également être automatiquement déclenchés par des programmes. le thread du minuteur dans le navigateur déclenchera l'événement du minuteur une fois le minuteur terminé. Le thème de cet article La boucle événementielle est en fait un ensemble de processus permettant de gérer et d'exécuter des événements dans un modèle événementiel.

Prenons une scène simple comme exemple. Supposons qu'il y ait un bouton de déplacement et un modèle de personnage sur l'interface du jeu. Chaque fois que vous cliquez pour vous déplacer vers la droite, la position du modèle de personnage doit être restituée et déplacée vers la droite. de 1 pixel. Nous pouvons l'implémenter de différentes manières en fonction du timing de rendu.

Première méthode de mise en œuvre : pilotée par les événements. Après avoir cliqué sur le bouton, lorsque la coordonnée positionX est modifiée, l'événement de rendu de l'interface est immédiatement déclenché et le re-rendu est déclenché.

Méthode de mise en œuvre deux : pilotée par l'état ou pilotée par les données. Après avoir cliqué sur le bouton, seule la coordonnée positionX est modifiée et le rendu de l'interface n'est pas déclenché. Avant cela, un timer setInterval sera démarré ou requestAnimationFrame sera utilisé pour détecter en continu si la positionX change. S'il y a des changements, effectuez un nouveau rendu immédiatement.

Le traitement des événements de clic dans les navigateurs est également généralement piloté par les événements. Dans un système piloté par événements, lorsqu'un événement est déclenché, les événements déclenchés seront temporairement stockés dans une file d'attente afin qu'une fois la tâche de synchronisation JS terminée, les événements à traiter soient retirés de cette file d'attente et traités. Ainsi, quand récupérer les tâches et quelles tâches récupérer en premier, cela est contrôlé par le processus de boucle d'événements.

Boucle d'événements dans le navigateur

Pile d'exécution et file d'attente des tâches

Lorsque JS analyse un morceau de code, il organisera le code de synchronisation quelque part dans l'ordre, c'est-à-dire la pile d'exécution, puis exécutera les fonctions à l'intérieur dans l'ordre. Lorsqu'une tâche asynchrone est rencontrée, elle est transmise à d'autres threads pour traitement. Une fois tous les codes de synchronisation de la pile d'exécution actuelle exécutés, les rappels des tâches asynchrones terminées seront retirés d'une file d'attente et ajoutés à la pile d'exécution. pour continuer l'exécution. Lorsqu'une tâche asynchrone est rencontrée, elle sera à nouveau traitée. Une fois les autres tâches asynchrones terminées, le rappel est placé dans la file d'attente des tâches pour être retiré de la pile d'exécution pour exécution.

JS exécute les méthodes de la pile d'exécution dans l'ordre.Chaque fois qu'une méthode est exécutée, un environnement d'exécution unique (contexte) sera généré pour cette méthode. Une fois l'exécution de cette méthode terminée, l'environnement d'exécution actuel est détruit et. supprimé de la pile. Cette méthode apparaît (c'est-à-dire que la consommation est terminée), puis passe à la méthode suivante.

On peut voir que dans le mode événementiel, au moins une boucle d'exécution est incluse pour détecter s'il y a de nouvelles tâches dans la file d'attente des tâches. En effectuant une boucle continue pour supprimer les rappels asynchrones pour l'exécution, ce processus est une boucle d'événement et chaque boucle est un cycle d'événement ou un tick.

Macro-tâches et micro-tâches

Il existe plus d'une file d'attente de tâches Selon le type de tâche, elle peut être divisée en file d'attente de micro-tâches et de macro-tâches.

Dans le processus de boucle d'événements, une fois l'exécution du code de synchronisation terminée, la pile d'exécution vérifie d'abord s'il y a des tâches dans la file d'attente des microtâches qui doivent être exécutées. Sinon, accédez à la file d'attente des macrotâches pour vérifier. s'il y a des tâches à exécuter, etc. Les microtâches sont généralement exécutées en premier dans le cycle en cours, tandis que les macrotâches attendront jusqu'au cycle suivant. Par conséquent, les microtâches sont généralement exécutées avant les macrotâches, et il n'y a qu'une seule file d'attente de microtâches, alors qu'il peut y avoir plusieurs files d'attente de macrotâches. De plus, nos événements courants de clic et de clavier appartiennent également aux tâches macro. Jetons un coup d'œil aux macro-tâches et aux micro-tâches courantes.

Tâches macrocomniques:

SetTimeout ()
  • SetInterval ()
  • seMmediate ()
Common Micro Tasks:

promise.then (), promest.catch ()
  • New MutaionObserver()
  • process.nextTick()
  • console.log('同步代码1');setTimeout(() => {    console.log('setTimeout')
    }, 0)new Promise((resolve) => {  console.log('同步代码2')  resolve()
    }).then(() => {    console.log('promise.then')
    })console.log('同步代码3');// 最终输出"同步代码1"、"同步代码2"、"同步代码3"、"promise.then"、"setTimeout"
  • Le code ci-dessus sera affiché dans l'ordre suivant : "Code de synchronisation 1", "Code de synchronisation 2", "Code de synchronisation 3", "promise.then", " setTimeout", l'analyse spécifique est la suivante.

(1) Le rappel setTimeout et promise.then sont tous deux exécutés de manière asynchrone et seront exécutés après tout le code synchrone

À propos, si le délai setTimeout est défini sur 0 dans le navigateur, il sera par défaut de 4 ms ; , NodeJS est de 1 ms. La valeur exacte peut varier, mais elle n'est pas 0.

(2) Bien que promise.then soit écrit à la fin, l'ordre d'exécution est antérieur à setTimeout car il s'agit d'une microtâche

(3) new Promise est exécuté de manière synchrone et le rappel dans promise.then est asynchrone ; de.

Jetons un coup d'œil à la démonstration du processus d'exécution du code ci-dessus :

Certaines personnes le comprennent également de cette façon : les micro-tâches sont exécutées à la fin de la boucle d'événements en cours ; la prochaine boucle d'événement. Jetons un coup d'œil à la différence essentielle entre les microtâches et les macrotâches.

Nous savons déjà que lorsque JS rencontre une tâche asynchrone, il confiera la tâche à d'autres threads pour traitement et son thread principal continuera à effectuer des tâches synchrones. Par exemple, le timing de setTimeout sera géré par le thread du minuteur du navigateur. Lorsque le timing se termine, la tâche de rappel du timer sera placée dans la file d'attente des tâches et attendra que le thread principal la retire pour exécution. Nous avons mentionné plus tôt que, étant donné que JS est exécuté dans un seul thread, pour effectuer des tâches asynchrones, d'autres threads de navigateur sont nécessaires pour vous aider. Autrement dit, le multithreading est une fonctionnalité évidente des tâches asynchrones JS.

Analysons le traitement de promise.then (microtâche). Lorsque promise.then est exécuté, le moteur V8 ne transmettra pas la tâche asynchrone aux autres threads du navigateur, mais stockera le rappel dans sa propre file d'attente. Une fois l'exécution de la pile d'exécution actuelle terminée, il exécutera immédiatement le. file d'attente où promise.then est stocké., les microtâches promise.then n'impliquent pas de multithreading. Même de certains points de vue, les microtâches ne peuvent pas être complètement asynchrones. Cela change simplement l'ordre d'exécution du code lors de l'écriture.

setTimeout a la tâche de "timing wait", qui doit être exécutée par le thread timer ; la requête ajax a la tâche "d'envoyer une requête", qui doit être exécutée par le thread HTTP, tandis que promise.then le fait n'a pas de tâches asynchrones qui doivent être exécutées par d'autres threads, il n'a que des rappels. Même s'il y en a, c'est juste une autre tâche de macro imbriquée à l'intérieur.

Un bref résumé des différences essentielles entre les microtâches et les macrotâches.

  • Fonctionnalités des tâches macro : Certaines tâches asynchrones claires doivent être exécutées et des rappels sont requis ;
  • Fonctionnalités de micro-tâches : Il n'y a pas de tâches asynchrones claires à exécuter, seulement des rappels ; aucune autre prise en charge de threads asynchrones n'est requise.

Erreur de minuterie

Dans la boucle d'événements, le code synchrone est toujours exécuté en premier, puis le rappel asynchrone est récupéré de la file d'attente des tâches pour exécution. Lorsque setTimeout est exécuté, le navigateur démarre un nouveau thread pour chronométrer l'appel. Après l'expiration du minuteur, l'événement timer est déclenché et le rappel est stocké dans la file d'attente des tâches de macro, en attendant que le thread principal JS termine l'exécution. Si le thread principal exécute toujours la tâche de synchronisation à ce moment-là, la tâche macro à ce moment devra d'abord être suspendue, ce qui entraînera le problème d'un minuteur inexact. Plus le code de synchronisation prend du temps, plus l'erreur dans la minuterie est importante. Non seulement le code de synchronisation, car les microtâches seront exécutées en premier, les microtâches affecteront également le timing. S'il y a une boucle infinie dans le code de synchronisation ou si la récursion dans la microtâche démarre constamment d'autres microtâches, alors le code dans la macrotâche. ne pourra jamais être obtenu. Par conséquent, il est très important d’améliorer l’efficacité d’exécution du code du thread principal.

Un scénario très simple est qu'il y a une horloge sur notre interface qui est précise à la seconde près et met à jour l'heure chaque seconde. Vous remarquerez que parfois les secondes sautent simplement l'intervalle de 2 secondes, et c'est pourquoi.

Rendu de mise à jour de la vue

Une fois l'exécution de la file d'attente des microtâches terminée, c'est-à-dire après la fin d'une boucle d'événement, le navigateur effectuera le rendu de la vue. Bien sûr, il y aura une optimisation du navigateur ici, et les résultats de plusieurs boucles peuvent être effectués. être fusionné pour effectuer un rafraîchissement de la vue Dessin, de sorte que la vue est mise à jour après la boucle d'événement, donc toutes les opérations sur le Dom ne rafraîchiront pas nécessairement la vue immédiatement. Le rappel requestAnimationFrame sera exécuté avant que la vue ne soit redessinée, il est donc controversé de savoir si requestAnimationFrame est une microtâche ou une macrotâche. À partir de là, il ne doit s'agir ni d'une microtâche ni d'une macrotâche.

Boucle d'événements dans NodeJS

Le moteur JS lui-même n'implémente pas le mécanisme de boucle d'événements. Celui-ci est implémenté par son hôte. La boucle d'événements dans le navigateur est principalement implémentée par le navigateur, et NodeJS a également sa propre boucle d'événements. . NodeJS dispose également d'un processus boucle + file d'attente de tâches et les microtâches sont prioritaires sur les macrotâches. Les performances générales sont cohérentes avec le navigateur. Cependant, il présente également quelques différences par rapport au navigateur et de nouveaux types de tâches et étapes de tâches ont été ajoutés. Ensuite, nous introduisons le processus de boucle d'événements dans NodeJS.

Méthodes asynchrones dans NodeJS

Parce qu'elles sont toutes basées sur le moteur V8, les méthodes asynchrones incluses dans le navigateur sont également les mêmes dans NodeJS. Il existe également d'autres formes courantes d'asynchrone dans NodeJS.

  • File I/O : chargez les fichiers locaux de manière asynchrone.
  • setImmediate() : similaire au réglage setTimeout de 0 ms, il sera exécuté immédiatement une fois certaines tâches de synchronisation terminées.
  • process.nextTick() : s'exécute immédiatement après que certaines tâches de synchronisation sont terminées.
  • server.close, socket.on('close',...), etc. : rappel de fermeture.

Imaginez, si le formulaire ci-dessus existe en même temps que setTimeout, promise, etc., comment analyser l'ordre d'exécution du code ? Tant que nous comprendrons le mécanisme de boucle d'événements de NodeJS, ce sera clair.

Modèle de boucle d'événement

Les capacités multiplateformes et le mécanisme de boucle d'événement de NodeJS sont tous implémentés sur la base de la bibliothèque Libuv. Vous n'avez pas besoin de vous soucier du contenu spécifique de cette bibliothèque. Il suffit de savoir que la bibliothèque Libuv est basée sur les événements et encapsule et unifie les implémentations d'API sur différentes plates-formes. Dans

NodeJS, le moteur V8 analyse le code JS et appelle l'API Node, puis transmet la tâche à Libuv pour allocation, et renvoie enfin les résultats d'exécution au moteur V8. Un ensemble de processus de boucle d'événements sont implémentés dans Libux pour gérer l'exécution de ces tâches, de sorte que la boucle d'événements de NodeJS est principalement complétée dans Libuv.

Voyons à quoi ressemblent les boucles dans Libuv.

Chaque étape de la boucle d'événements

Dans l'exécution de JS dans NodeJS, le processus dont nous devons principalement nous soucier est divisé en étapes suivantes. Chaque étape ci-dessous a sa propre file d'attente de tâches distincte. Lorsque l'étape correspondante est exécutée, le courant Indique s'il y a des tâches qui doivent être traitées dans la file d'attente des tâches de l'étape.

  • timers 阶段:执行所有 setTimeout() 和 setInterval() 的回调。
  • pending callbacks 阶段:某些系统操作的回调,如  TCP  链接错误。除了 timers、close、setImmediate 的其他大部分回调在此阶段执行。
  • poll 阶段:轮询等待新的链接和请求等事件,执行 I/O 回调等。V8 引擎将 JS 代码解析并传入 Libuv 引擎后首先进入此阶段。如果此阶段任务队列已经执行完了,则进入 check 阶段执行 setImmediate 回调(如果有 setImmediate),或等待新的任务进来(如果没有 setImmediate)。在等待新的任务时,如果有 timers 计时到期,则会直接进入 timers 阶段。此阶段可能会阻塞等待。
  • check 阶段:setImmediate 回调函数执行。
  • close callbacks 阶段:关闭回调执行,如 socket.on('close', ...)。

上面每个阶段都会去执行完当前阶段的任务队列,然后继续执行当前阶段的微任务队列,只有当前阶段所有微任务都执行完了,才会进入下个阶段。这里也是与浏览器中逻辑差异较大的地方,不过浏览器不用区分这些阶段,也少了很多异步操作类型,所以不用刻意去区分两者区别。代码如下所示:

const fs = require('fs');
fs.readFile(__filename, (data) => {    // poll(I/O 回调) 阶段
    console.log('readFile')    Promise.resolve().then(() => {        console.error('promise1')
    })    Promise.resolve().then(() => {        console.error('promise2')
    })
});setTimeout(() => {    // timers 阶段
    console.log('timeout');    Promise.resolve().then(() => {        console.error('promise3')
    })    Promise.resolve().then(() => {        console.error('promise4')
    })
}, 0);// 下面代码只是为了同步阻塞1秒钟,确保上面的异步任务已经准备好了var startTime = new Date().getTime();var endTime = startTime;while(endTime - startTime < 1000) {
    endTime = new Date().getTime();
}// 最终输出 timeout promise3 promise4 readFile promise1 promise2

另一个与浏览器的差异还体现在同一个阶段里的不同任务执行,在 timers 阶段里面的宏任务、微任务测试代码如下所示:

setTimeout(() => {  console.log('timeout1')    Promise.resolve().then(function() {    console.log('promise1')
  })
}, 0);setTimeout(() => {  console.log('timeout2')    Promise.resolve().then(function() {    console.log('promise2')
  })
}, 0);
  • 浏览器中运行

    每次宏任务完成后都会优先处理微任务,输出“timeout1”、“promise1”、“timeout2”、“promise2”。

  • NodeJS 中运行

    因为输出 timeout1 时,当前正处于  timers 阶段,所以会先将所有 timer 回调执行完之后再执行微任务队列,即输出“timeout1”、“timeout2”、“promise1”、“promise2”。

上面的差异可以用浏览器和 NodeJS 10 对比验证。是不是感觉有点反程序员?因此 NodeJS 在版本 11 之后,就修改了此处逻辑使其与浏览器尽量一致,也就是每个 timer 执行后都先去检查一下微任务队列,所以 NodeJS 11 之后的输出已经和浏览器一致了。

nextTick、setImmediate 和 setTimeout

实际项目中我们常用 Promise 或者 setTimeout 来做一些需要延时的任务,比如一些耗时计算或者日志上传等,目的是不希望它的执行占用主线程的时间或者需要依赖整个同步代码执行完成后的结果。

NodeJS 中的 process.nextTick() 和 setImmediate() 也有类似效果。其中 setImmediate() 我们前面已经讲了是在 check 阶段执行的,而 process.nextTick() 的执行时机不太一样,它比 promise.then() 的执行还早,在同步任务之后,其他所有异步任务之前,会优先执行 nextTick。可以想象是把 nextTick 的任务放到了当前循环的后面,与 promise.then() 类似,但比 promise.then() 更前面。意思就是在当前同步代码执行完成后,不管其他异步任务,先尽快执行 nextTick。如下面的代码,因此这里的 nextTick 其实应该更符合“setImmediate”这个命名才对。

setTimeout(() => {    console.log('timeout');
}, 0);Promise.resolve().then(() => {    console.error('promise')
})
process.nextTick(() => {    console.error('nextTick')
})// 输出:nextTick、promise、timeout

接下来我们再来看看 setImmediate 和 setTimeout,它们是属于不同的执行阶段了,分别是 timers 阶段和 check 阶段。

setTimeout(() => {  console.log('timeout');
}, 0);setImmediate(() => {  console.log('setImmediate');
});// 输出:timeout、 setImmediate

分析上面代码,第一轮循环后,分别将 setTimeout   和 setImmediate 加入了各自阶段的任务队列。第二轮循环首先进入  timers 阶段,执行定时器队列回调,然后  pending callbacks 和 poll 阶段没有任务,因此进入check 阶段执行 setImmediate 回调。所以最后输出为“timeout”、“setImmediate”。当然这里还有种理论上的极端情况,就是第一轮循环结束后耗时很短,导致 setTimeout 的计时还没结束,此时第二轮循环则会先执行 setImmediate 回调。

再看这下面一段代码,它只是把上一段代码放在了一个 I/O 任务回调中,它的输出将与上一段代码相反。

const fs = require('fs');
fs.readFile(__filename, (data) => {    console.log('readFile');    setTimeout(() => {        console.log('timeout');
    }, 0);    setImmediate(() => {        console.log('setImmediate');
    });
});// 输出:readFile、setImmediate、timeout

如上面代码所示:

  • Il n'y a pas de file d'attente de tâches asynchrones qui doivent être exécutées lors du premier tour de boucle ;
  • Il n'y a pas de tâches dans les minuteries et autres étapes du deuxième tour de boucle, seule l'étape d'interrogation a des tâches de rappel d'E/S, qui génère "readFile" ;
  • Référez-vous à l'étape d'événement précédente. Notez qu'ensuite, la phase d'interrogation détectera s'il existe une file d'attente de tâches setImmediate et entrera dans la phase de vérification. Sinon, elle sera jugée s'il y a un rappel de tâche de minuterie. , il reviendra à la phase des minuteries. Par conséquent, il doit entrer dans la phase de vérification pour exécuter setImmediate et afficher "setImmediate" ;
  • Entrez ensuite dans l'étape finale des rappels de fermeture, et ce cycle se termine
  • Passez enfin au troisième tour de ; boucle, entrez dans l'étape des minuteries et affichez "timeout".

Donc, la sortie finale de "setImmediate" est avant "timeout". On peut voir que l'ordre d'exécution des deux est lié à l'étape d'exécution en cours.

【Recommandations associées : Tutoriel vidéo JavaScript, front-end web

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!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer