Débuter avec les travailleurs Web

L'un des nombreux objectifs de la conception du langage JavaScript était de le garder simple à la fois et, par extension, simple. Bien que je dois admettre que, compte tenu des particularités des constructions de langage, c'est tout sauf simple! Mais ce que nous entendons par "single-threaded", c'est qu'il n'y a qu'un seul thread de contrôle en JavaScript; oui, malheureusement, votre moteur JavaScript ne peut faire qu'une chose à la fois.

Cela ne vous semble-t-il pas trop contraignant d'utiliser des processeurs multicœurs inactifs sur votre machine? HTML5 promet de changer tout cela.


Modèle à thread unique de JavaScript

Les Web Workers vivent dans un monde restreint sans accès DOM, car DOM n'est pas thread-safe.

Une école de pensée considère le caractère à thread unique de JavaScript comme une simplification, tandis que l’autre l’écarte comme une limitation. Ce dernier groupe a un très bon point, en particulier lorsque les applications Web modernes utilisent intensivement JavaScript pour gérer les événements de l'interface utilisateur, interroger ou interroger les API côté serveur, traiter de grandes quantités de données et manipuler le DOM en fonction de la réponse du serveur..

Pouvoir faire autant dans un seul thread de contrôle tout en conservant une interface utilisateur réactive est souvent une tâche ardue et oblige les développeurs à recourir à des hacks et à des solutions de contournement (telles que l'utilisation de setTimeout (), setInterval (), ou en utilisant XMLHttpRequest et événements DOM) pour obtenir la simultanéité. Cependant, il est intéressant de noter que ces techniques fournissent un moyen de passer des appels asynchrones, mais non bloquant ne signifie pas nécessairement simultané. John Resig explique pourquoi vous ne pouvez rien exécuter en parallèle sur son blog.

Les limites

Si vous avez utilisé JavaScript pendant un laps de temps raisonnable, il est fort probable que vous ayez rencontré la boîte de dialogue suivante qui vous gêne en indiquant que l'exécution d'un script est trop longue. Oui, presque chaque fois que votre page cesse de répondre, la raison peut être attribuée à un code JavaScript..

Voici quelques-unes des raisons pour lesquelles votre navigateur risque de raccrocher en exécutant votre script:

  • Manipulation DOM excessive: La manipulation DOM est peut-être l'opération la plus coûteuse que vous puissiez effectuer avec JavaScript. Par conséquent, de nombreuses opérations de manipulation du DOM font de votre script un bon candidat pour le refactoring..
  • Des boucles sans fin: Il n’est jamais inutile d'analyser votre code pour rechercher des boucles imbriquées complexes. Celles-ci ont tendance à faire beaucoup plus de travail que ce qui est réellement nécessaire. Peut-être que vous pouvez trouver une solution différente qui offre la même fonctionnalité.
  • Combinant les deux: Le pire que nous puissions faire est de mettre à jour le DOM à plusieurs reprises au sein d'une boucle lorsque des solutions plus élégantes, telles que l'utilisation d'un DocumentFragment, existent.

Web Workers à la rescousse

… Non bloquant ne signifie pas nécessairement simultané…

Grâce à HTML5 et aux travailleurs Web, vous pouvez maintenant créer un nouveau fil - fournissant une véritable asynchronie. Le nouveau travailleur peut s'exécuter en arrière-plan pendant que le thread principal traite les événements de l'interface utilisateur, même si le thread de travail est occupé à traiter une grande quantité de données. Par exemple, un travailleur peut traiter une structure JSON volumineuse pour extraire des informations précieuses à afficher dans l'interface utilisateur. Mais assez de mes bavardages; voyons du code en action.

Créer un ouvrier

Normalement, le code correspondant à un outil de traitement Web réside dans un fichier JavaScript séparé. Le fil parent crée un nouvel ouvrier en spécifiant l'URI du fichier de script dans Ouvrier constructeur, qui charge et exécute le fichier JavaScript de manière asynchrone.

var primeWorker = new Worker ('prime.js');

Commencer un travailleur

Pour démarrer un travailleur, le thread parent envoie un message au travailleur, comme suit:

var current = $ ('# prime'). attr ('valeur'); primeWorker.postMessage (en cours);

La page parent peut communiquer avec les travailleurs à l’aide de la postMessage API, qui est également utilisée pour la messagerie d’origine croisée. Outre l'envoi de types de données primitifs au travailleur, le postMessage L'API prend également en charge le passage de structures JSON. Cependant, vous ne pouvez pas transmettre de fonctions car elles peuvent contenir des références au DOM sous-jacent..

Les threads parent et ouvrier ont leur propre espace séparé; les messages échangés sont copiés plutôt que partagés.

En coulisse, ces messages sont sérialisés chez le travailleur, puis désérialisés chez le destinataire. Pour cette raison, il est déconseillé d’envoyer d’énormes quantités de données au travailleur..

Le thread parent peut également enregistrer un rappel pour écouter les messages postés par le travailleur après avoir exécuté sa tâche. Cela permet au thread parent de prendre les mesures nécessaires (comme la mise à jour du DOM) une fois que le travailleur a joué son rôle. Regardez ce code:

primeWorker.addEventListener ('message', fonction (événement) console.log ('Réception de la part de l'opérateur:' + événement.data); $ ('# prime'). html (événement.data););

le un événement objet contient deux propriétés importantes:

  • cible: utilisé pour identifier le travailleur qui a envoyé le message; principalement utile dans un environnement à plusieurs travailleurs.
  • Les données: le message envoyé par le travailleur à son thread parent.

Le travailleur lui-même est contenu dans prime.js et enregistre pour le message événement, qu'il reçoit de son parent. Il utilise également le même postMessage API pour communiquer avec le thread parent.

self.addEventListener ('message', fonction (événement) var currPrime = event.data, nextPrime; setInterval (function () nextPrime = getNextPrime (currPrime); postMessage (nextPrime); currPrime = nextPrime;, 500); )

Les travailleurs Web vivent dans un environnement restreint et thread-safe.

Dans cet exemple, nous trouvons simplement le prochain nombre premier le plus élevé et publions à plusieurs reprises les résultats dans le thread parent, ce qui met à jour l'interface utilisateur avec la nouvelle valeur. Dans le contexte d'un travailleur, les deux soi et ce se référer à la portée globale. Le travailleur peut soit ajouter un écouteur d'événements pour le message événement, ou il peut définir la message gestionnaire pour écouter les messages envoyés par le thread parent.

La tâche de trouver le prochain nombre premier n'est évidemment pas le cas d'utilisation idéal pour un travailleur, mais a été choisie ici pour illustrer le concept de transmission de messages. Plus tard, nous explorons des cas d’utilisation possibles et pratiques dans lesquels l’utilisation d’un Web Worker serait réellement rentable..

Travailleurs licenciés

Les travailleurs utilisent beaucoup de ressources; ce sont des threads au niveau du système d'exploitation. Par conséquent, vous ne souhaitez pas créer un grand nombre de threads de travail et vous devez mettre fin au programme de travail Web après son travail. Les travailleurs peuvent se terminer, comme ceci:

auto.close ();

Ou un thread parent peut mettre fin à un ouvrier:

primeWorker.terminate ();

Sécurité et restrictions

Dans un script de travail, nous n’avons pas accès aux nombreux objets JavaScript importants tels que document, la fenêtre, console, parent et surtout pas d'accès au DOM. Ne pas avoir accès au DOM et ne pas pouvoir mettre à jour la page semble trop restrictif, mais c’est une décision de conception de sécurité importante. Imaginez les dégâts que cela pourrait causer si plusieurs threads essayaient de mettre à jour le même élément. Ainsi, les travailleurs Web vivent dans un environnement restreint et thread-safe.

Ceci dit, vous pouvez toujours utiliser des ouvriers pour traiter les données et renvoyer le résultat au thread principal, qui peut ensuite mettre à jour le DOM. Bien que l'accès à certains objets JavaScript assez importants leur soit refusé, les utilisateurs sont autorisés à utiliser certaines fonctions telles que setTimeout () / clearTimeout (), setInterval () / clearInterval (), navigateur, etc. Vous pouvez également utiliser le XMLHttpRequest et stockage local objets à l'intérieur du travailleur.

Restrictions de même origine

Dans le contexte d'un travailleur, les deux soi et ce se référer à la portée globale.

Pour pouvoir communiquer avec un serveur, les travailleurs doivent suivre la stratégie de même origine. Par exemple, un script hébergé sur http://www.example.com/ ne peut pas accéder à un script sur https://www.example.com/. Même si les noms d'hôte sont identiques, la même stratégie d'origine stipule que le protocole doit être identique. Normalement, ce n'est pas un problème. Il est fort probable que vous écriviez à la fois le travailleur et le client et que vous les serviez à partir du même domaine, mais connaître la restriction est toujours utile..

Problèmes d'accès local avec Google Chrome

Google Chrome impose des restrictions sur l'accès aux utilisateurs au niveau local. Par conséquent, vous ne pourrez pas exécuter ces exemples sur une configuration locale. Si vous souhaitez utiliser Chrome, vous devez héberger ces fichiers sur un serveur ou utiliser le --autoriser-fichier-accès-de-fichiers drapeau lors du démarrage de Chrome à partir de la ligne de commande. Pour OS X, démarrez Chrome comme suit:

$ / Applications / Google \ Chrome.app/Contents/MacOS/Google \ Chrome --allow-file-access-from-files

Toutefois, l'utilisation de cet indicateur n'est pas recommandée dans un environnement de production. Par conséquent, le mieux est d’héberger ces fichiers sur un serveur Web et de tester vos travailleurs Web dans n’importe quel navigateur pris en charge..

Débogage des travailleurs et gestion des erreurs

Ne pas avoir accès à console cela rend quelque peu non trivial, mais grâce aux outils de développement Chrome, on peut déboguer le code ouvrier comme s'il s'agissait d'un autre code JavaScript.

Pour gérer les erreurs générées par les travailleurs Web, vous pouvez écouter les Erreur event, qui renseigne un objet ErrorEvent. Vous pouvez inspecter cet objet pour connaître la cause détaillée de l'erreur..

primeWorker.addEventListener ('error', function (error) console.log ('Erreur provoquée par le travailleur:' + error.filename + 'à la ligne:' + error.lineno + 'Message détaillé:' + error.message) ;);

Fils de plusieurs travailleurs

Bien qu'il soit courant d'avoir plusieurs threads de travail divisant le travail entre eux, une mise en garde s'impose. La spécification officielle précise que ces travailleurs ont un poids relativement important et sont censés être des scripts à longue durée de vie exécutés à l’arrière-plan. Les employés Web ne sont pas conçus pour être utilisés en grand nombre en raison de leur coût élevé de performances de démarrage et de leur coût de mémoire par instance élevé.

Brève introduction aux travailleurs partagés

La spécification décrit deux types de travailleurs: dédiés et partagés. Jusqu'à présent, nous avons vu des exemples de travailleurs dévoués. Ils sont directement liés à leur script / page de créateur en ce sens qu'ils entretiennent une relation un à un avec le script / la page qui les a créés. Les travailleurs partagés, en revanche, peuvent être partagés entre toutes les pages d’une origine (c.-à-d. Que toutes les pages ou tous les scripts de la même origine peuvent communiquer avec un travailleur partagé)..

Pour créer un travailleur partagé, il suffit de transmettre l'URL du script ou le nom du travailleur au constructeur SharedWorker..

La principale différence dans la manière dont les travailleurs partagés sont utilisés est qu’ils sont associés à une Port suivre le script parent qui y accède.

L'extrait de code suivant crée un travailleur partagé, enregistre un rappel pour écouter les messages postés par le travailleur et publie un message sur le travailleur partagé:

var sharedWorker = new SharedWorker ('findPrime.js'); sharedWorker.port.onmessage = function (event) … sharedWorker.port.postMessage ('données que vous souhaitez envoyer');

De même, un travailleur peut écouter les relier événement, qui est reçu lorsqu'un nouveau client tente de se connecter au travailleur et lui envoie ensuite un message en conséquence.

onconnect = function (event) // event.source contient la référence au port du client var clientPort = event.source; // écoute les messages envoyés à ce client clientPort.onmessage = function (event) // event.data contient le message envoyé par le client var data = event.data;… // Post Data après le traitement clientPort.postMessage ('traité Les données'); ;

En raison de leur nature partagée, vous pouvez conserver le même état dans différents onglets de la même application, car les deux pages de différents onglets utilisent le même script de travail partagé pour gérer et signaler l'état. Pour plus de détails sur les travailleurs partagés, je vous encourage à lire la spécification.


Cas d'utilisation pratique

Les employés Web ne sont pas conçus pour être utilisés en grand nombre en raison de leur coût élevé de performances de démarrage et de leur coût de mémoire par instance élevé.

Un scénario réel pourrait être lorsque vous êtes obligé de traiter avec une API tierce synchrone qui oblige le thread principal à attendre un résultat avant de passer à l'instruction suivante. Dans ce cas, vous pouvez déléguer cette tâche à un agent nouvellement créé pour exploiter au mieux la fonctionnalité asynchrone..

Les travailleurs Web excellent également dans les situations d'interrogation où vous interrogez en permanence une destination en arrière-plan et envoyez un message au thread principal lorsque de nouvelles données arrivent..

Vous devrez peut-être également traiter une quantité énorme de données renvoyées par le serveur. Traditionnellement, le traitement d'un grand nombre de données a un impact négatif sur la réactivité de l'application, rendant l'expérience utilisateur inacceptable. Une solution plus élégante diviserait le travail de traitement entre plusieurs travailleurs pour traiter des parties de données qui ne se chevauchent pas.

D'autres cas d'utilisation pourraient être l'analyse de sources vidéo ou audio avec l'aide de plusieurs travailleurs Web, chacun travaillant sur une partie prédéfinie du problème..


Conclusion

Imaginez la puissance associée à plusieurs threads dans un environnement à un seul thread.

Comme pour beaucoup d'éléments de la spécification HTML5, la spécification du travailleur Web continue d'évoluer. Si vous envisagez de travailler sur le Web, cela ne fera pas de mal de donner un aperçu de la spécification.

La prise en charge inter-navigateurs est assez bonne pour les utilisateurs dédiés disposant des versions actuelles de Chrome, Safari et Firefox. Même IE ne traîne pas trop loin derrière avec IE10 qui prend les choses en main. Toutefois, les utilisateurs partagés ne sont pris en charge que sur les versions actuelles de Chrome et Safari. Étonnamment, la dernière version du navigateur Android disponible dans Android 4.0 ne prend pas en charge les travailleurs Web, bien qu'ils aient été pris en charge dans la version 2.1. Apple a également inclus la prise en charge des travailleurs Web à partir de iOS 5.0..

Imaginez la puissance associée à plusieurs threads dans un environnement à un seul thread. Les possibilités sont infinies!