Maîtrise HTML5 le contexte de navigation

Lorsque nous créons un nouvel onglet dans le navigateur, un nouveau contexte de navigation est créé. Un contexte de navigation regroupe un grand nombre de choses différentes que nous prenons pour acquis: des choses comme la gestion des sessions et de l'historique, la navigation et le chargement des ressources. Nous avons également une boucle d'événement, utilisée pour traiter l'interaction entre les scripts et le DOM..

Dans ce tutoriel, nous allons d’abord acquérir une compréhension essentielle de la boucle d’événements, qui est principalement conçue selon le modèle de réacteur. Ensuite, examinons quelques objets auxiliaires associés au contexte de navigation. Enfin, nous établirons une liste des meilleures pratiques pour utiliser la boucle d’événements aussi efficacement que possible..

Le modèle de réacteur

Chaque contexte de navigation est accompagné d'une boucle d'événement associée. La boucle d'événement est liée à la la fenêtre du document actuel du contexte. Il peut être partagé avec un autre la fenêtre qui accueille un document de même origine.

La boucle d’événements prend en charge l’exécution des événements de manière normalisée, c’est-à-dire qu’ils ne peuvent pas s’entrelacer ni s’exécuter en parallèle. Par exemple, même si Ouvrier nous permet d’exécuter un autre JavaScript en parallèle, nous ne pouvons pas interagir directement avec le DOM ou le JavaScript de base. Nous devons envoyer des messages, qui sont traités via des événements. Ces événements sont traités par la boucle d’événements et seront donc exécutés dans un ordre séquentiel..

La plupart des boucles d'événements sont implémentées dans une sorte de modèle de réacteur. Une des raisons est que tous les systèmes de réacteurs sont mono-threadés par conception, mais peuvent bien entendu exister dans des environnements multi-threadés. La structure d'un réacteur est la suivante:

  • Nous avons des ressources (présentées sous forme de descripteurs, c'est-à-dire des références, telles que des rappels JavaScript).
  • Un démultiplexeur pour normaliser l'exécution des événements.
  • Un répartiteur qui enregistre / désenregistre les gestionnaires et envoie les événements.
  • Bien sûr, nous avons également besoin de gestionnaires d’événements pour utiliser les ressources référencées avec les descripteurs..

Le démultiplexeur envoie le descripteur au répartiteur dès que possible, c'est-à-dire une fois que tous les événements précédents ont été gérés. En interne, le démultiplexeur peut avoir une sorte de file d'attente, fonctionnant selon le principe du premier entré, premier sorti (FIFO). Les implémentations réelles peuvent utiliser une sorte de file d'attente prioritaire.

Le schéma suivant illustre la relation entre les différents composants du modèle de réacteur.

Dans le cas de l'exécution de JavaScript dans le navigateur, le descripteur est une fonction JavaScript, permettant de capturer certains objets. Les gestionnaires d'événements sont des gestionnaires d'événements DOM. Le réacteur permet au code de fonctionner simultanément sans aucun problème de cross-threading.

Il existe plusieurs répartiteurs, puisque, par exemple, tous les EventTarget est capable d’enregistrer, de désenregistrer et d’envoyer des événements. Cependant, un seul démultiplexeur est attribué par boucle d'événement..

Parcourir la boucle d'événement

Maintenant que nous avons une compréhension vague du modèle de réacteur et de son implémentation dans une boucle d'événements, nous devons examiner de plus près la boucle d'événements du point d'interaction, qui est JavaScript dans notre cas..

Chaque fois que nous effectuons un appel de fonction en JavaScript, un cadre est ajouté à la pile d'appels par le runtime. La pile a une structure dernier entré, premier sorti (LIFO). La pile d'appels garde une trace du chemin d'appel. Une trame est constituée des arguments de la fonction et de son état local, définis par l'instruction en cours d'exécution, ainsi que de toutes les variables locales avec leurs valeurs correspondantes..

Pour illustrer ce concept, considérons le code suivant.

function init () setTimeout (function () , 0);  init (); 

Si nous sauvegardons maintenant la pile d'appels après chaque modification, nous obtenons l'image suivante. Nous commençons avec une pile d'appels vide. Après avoir appelé le init fonction nous avons un élément sur la pile. le setTimeout call nous donne un autre élément, même si la fonction elle-même ne fera que faire basculer le contexte vers une fonction native. Étant retourné au init méthode nous reste avec un seul élément sur la pile. Enfin la pile est à nouveau vide.

À ce stade, il devient logique de distinguer deux modes de déclenchement d’événements en JavaScript: synchrone et asynchrone. La différence peut être illustrée par l'exemple de code suivant. Nous utilisons un événement asynchrone (Cliquez sur) déclencheur pour exécuter un synchrone (concentrer) un. Notez que Cliquez sur peut également être déclenché de manière synchrone en utilisant le Cliquez sur() méthode.

var button = document.querySelector ('bouton'); var text = document.querySelector ('input [type = text]'); button.addEventListener ('click', function () console.log ('foo'); text.focus (); console.log ('bar');, false); text.addEventListener ('focus', function () console.log ('baz');, false); 

Nous verrons ça foo, baz et bar sera connecté. Par conséquent, la concentrer l'événement a été traité juste après l'appel concentrer(). Il existe également des événements, tels que les événements de mutation DOM, qui sont toujours déclenchés de manière synchrone. Nous discutons des événements de mutation DOM dans le huitième article de cette série.

L’implémentation du démultiplexeur d’événements pour la boucle d’événements dans un navigateur doit faire la distinction entre les rappels d’événements ordinaires en cours d’exécution (connus sous le nom de les tâches) et de plus petits morceaux de code préférés, nommés Microtasks.

Alors que les tâches sont mises en file d'attente de manière normale, les micro-tâches sont mises en file d'attente avec une priorité élevée. Ils sont exécutés dès que possible, c’est-à-dire toujours avant la tâche suivante. La plupart des interactions DOM sont mises en file d'attente dans la boucle d'événements en tant que tâches, par exemple. appelant la fonction de rappel de setTimeout. D'autre part, les rappels de la nouvelle Promettre les types sont appelés microtasks.

Un autre exemple d’API qui met en file d’attente des microtasks est le MutationObserver interface. Pour le moment, nous reportons la discussion de la MutationObserver au huitième article de cette série.

L'exemple suivant illustre la différence entre une tâche ordinaire et une microtâche..

function init () console.log ('foo'); setTimeout (plus tard, 0); à présent(); Promise.resolve (). Puis (bientôt); console.log ('bar');  function later () console.log ('baz');  function soon () console.log ('norf');  function now () console.log ('qux');  init (); 

Nous verrons ça foo, qux, bar, norf et baz sont connectés. L'ordre de norf et baz pourrait être erroné selon le navigateur, mais devrait être tel que présenté. L'exécution du précédent exemple de code dans la console du navigateur donne également un résultat intéressant.

À titre d'exemple, le résultat de la dernière version d'Opera est affiché ci-dessous..

Le résultat de la init une fonction (indéfini) est affiché après le rappel du Promettre a été exécuté. Par conséquent, les outils de débogage du navigateur sont intégrés à l'aide d'une tâche normale et non d'une microtache..

Bien que le navigateur puisse utiliser l'intervalle entre les tâches normales pour effectuer des étapes intrinsèques, telles que l'émission d'un appel de rendu, les micro-tâches sont exécutées immédiatement après l'exécution du code actuel..

Session et gestion de l'histoire

Un contexte de navigation contient beaucoup plus de services. La plupart de ces services ne sont généralement pas accessibles, mais certains d'entre eux peuvent exposer une API que nous pouvons utiliser. Par exemple, certains services basés sur le matériel sont exposés dans navigateur objet.

De plus, nous pouvons accéder à l’histoire (locale) via le l'histoire objet. Nous trouvons ici plusieurs méthodes utiles. Le plus utile est probablement pushState (). Il faut trois paramètres:

  1. L'état de l'entrée à pousser. Cela doit être une chaîne. Nous pourrions utiliser une structure de données JSON.
  2. Un titre pour notre référence. Ceci n'est pas utilisé par la plupart des navigateurs, nous pouvons donc omettre complètement ce paramètre.
  3. Enfin une URL à afficher. Les navigateurs l'afficheront généralement dans la barre d'adresse..

Voyons un exemple.

history.pushState (null, null, 'http://www.example.com/my-state'); 

Appel pushState changera immédiatement l'URL affichée. Que se passe-t-il lorsque l'utilisateur appuie sur le bouton de retour omniprésent? De manière générale, il appartient au navigateur de décider, mais une bonne décision serait d'afficher l'état actuel..

Les opérations en avant ou en arrière sont également exposées via le l'histoire objet. Par exemple, nous pouvons faire ce qui suit:

history.back (); history.forward (); 

Invoquer retour par programme ou via l'interface utilisateur peut conduire à revenir à la page précédente. Si la pile est déjà vide, nous ne pouvons pas afficher l'état actuel. Cela peut également être expérimenté avec une courte démo, qui implique deux boutons et une liste.

L'exemple de code pour déclencher la logique se présente comme suit.

var list = document.querySelector ('ul'); var buttons = document.querySelectorAll ('bouton'); var retour = boutons [0]; var forward = boutons [1]; back.addEventListener ('click', function (ev) history.back ();, false); forward.addEventListener ('click', fonction (ev) var c = list.childElementCount.toString (); var url = 'foo-' + c; history.pushState (c, null, url); var item = document createElement ('li'); item.textContent = url; list.appendChild (item);, false); window.addEventListener ('popstate', fonction (ev) list.removeChild (list.children [ev.state * 1]);, false); 

L’API d’historique est un bon moyen d’implémenter le routage dans une application à page unique (SPA). Une autre manière qui utilise toujours l’URL pour le routage est donnée en manipulant le hachage de l’URL et en écoutant le hashchange un événement. Encore une fois, nous utilisons des événements pour déclencher des rappels de manière asynchrone.

Les meilleures pratiques

Un inconvénient de la boucle d’événements est qu’une application Web dépend fortement du temps de traitement de la tâche ou de la microtache actuellement active. Nous ne sommes donc pas en mesure de mettre en file d'attente un calcul de longue durée. Le navigateur arrêterait le calcul après un certain temps. Une bonne pratique pour contourner ce problème consiste à scinder le calcul en plusieurs parties, qui sont traitées par la boucle d'événement l'une après l'autre..

Un schéma simple d'affichage de la boucle d'événement est présenté ci-dessous. La boucle tourne librement jusqu'à ce qu'une tâche soit en attente d'exécution. Si nous considérons que la tâche est liée à JavaScript, nous devons passer le contrôle au moteur JavaScript. Enfin, le code pourrait enregistrer des rappels supplémentaires. Si l'événement est déclenché, tous les rappels sont mis en file d'attente sur la boucle d'événements en tant que tâches à exécuter..

Dans les navigateurs, les rappels sont mis en file d'attente chaque fois que l'événement associé se produit. Les événements sans rappel ne rien mettre en file d'attente.

Appel setTimeout () attend au moins l'heure indiquée avant de mettre en file d'attente le rappel spécifié dans la file d'attente. S'il n'y a pas d'autre tâche dans la file d'attente, le rappel est immédiatement appelé, sinon nous devons attendre. C’est la raison pour laquelle le deuxième argument de setTimeout définit un temps minimum et non un temps garanti.

Les calculs longs doivent être placés dans un Ouvrier, qui fournit sa propre boucle d'événement. Il contient également sa propre gestion de la mémoire. Par conséquent, un travailleur Web n'interférera pas avec la boucle d'événements de notre application Web, car la communication se fait via des événements. Le schéma entier est non bloquant.

En général, nous devrions toujours essayer d'éviter d'utiliser du code bloquant. La plupart des API sont déjà exposées de manière non bloquante, en utilisant directement des rappels ou des événements. Malheureusement, il existe des exceptions héritées, telles que alerte ou XHR synchrone. Ils ne devraient jamais être utilisés à moins de savoir exactement ce que nous faisons..

Alors, devrions-nous utiliser une tâche ou une microtâche? le Promettre L'API utilise une microtâche pour une raison. Si possible, nous voudrons peut-être viser cela. La microtache est effectuée dès que possible, pratiquement immédiatement après l'exécution du code actuel. Cela pourrait donc empêcher un rendu inutile. Pourquoi devrions-nous rendre une fois avant d'insérer un résultat, ce qui nécessitera une autre opération de rendu? Cependant, en cas de doute, nous devrions redonner le contrôle au navigateur en utilisant une tâche standard..

Conclusion

Il est important de bien comprendre le fonctionnement de JavaScript et son interaction avec le DOM et d'autres ressources. Cela commence par la boucle d'événement. Le concept est non seulement implémenté dans le navigateur, mais est également présent dans le noyau de Node.js. Maîtriser JavaScript, c'est maîtriser la boucle d'événement.

Dans le précédent tutoriel, nous avons vu que le navigateur nous fournissait plusieurs mécanismes qui sont agrégés dans le contexte de navigation et exposés à plusieurs moments. Même si nous ne pouvons pas contrôler certains mécanismes internes, nous devons toujours savoir qu’ils existent et comment ils fonctionnent..

Références

  • JSConf 2014 - Que diable est l'événement Loop Anyway? (Philip Roberts)
  • La boucle d'événement JavaScript expliquée
  • La boucle d'événement Node.js
  • Comprendre la boucle d'événement JavaScript
  • Tâches, micro-tâches, files d'attente et horaires
  • Plongez dans HTML5 - Manipulation de l'historique pour le plaisir et le profit