De nombreux frameworks frontaux JavaScript (tels que Angular, React et Vue) ont leurs propres systèmes de réactivité. Comprendre les fonctionnalités de ces systèmes sera utile à tout développeur, l'aidera à utiliser plus efficacement les frameworks JS modernes.

Le matériel, dont nous publions la traduction aujourd'hui, montre un exemple étape par étape de développement d'un système de réactivité en JavaScript pur. Ce système implémente les mêmes mécanismes que ceux utilisés dans Vue.
Système de réactivité
Pour quelqu'un qui rencontre pour la première fois le système de réactivité Vue, cela peut sembler une mystérieuse boîte noire. Prenons une simple application Vue. Voici le balisage:
<div id="app">    <div>Price: ${{ price }}</div>    <div>Total: ${{ price*quantity }}</div>    <div>Taxes: ${{ totalPriceWithTax }}</div> </div> 
Voici la commande de connexion du framework et le code d'application.
 <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script>   var vm = new Vue({       el: '#app',       data: {           price: 5.00,           quantity: 2       },       computed: {           totalPriceWithTax() {               return this.price * this.quantity * 1.03           }       }   }) </script> 
D'une manière ou d'une autre, Vue découvre que lorsque le 
price change, le moteur doit faire trois choses:
- Actualisez pricevaleur dupricesur la page Web.
- Recalculez l'expression dans laquelle le priceest multiplié par laquantityet affichez la valeur résultante sur la page.
- Appelez la fonction totalPriceWithTaxet, encore une fois, mettez ce qu'elle renvoie sur la page.
Ce qui se passe ici est illustré dans l'illustration suivante.
Comment Vue sait-il quoi faire lorsque la propriété de prix change?Nous avons maintenant des questions sur la façon dont Vue sait exactement ce qui doit être mis à jour lorsque le 
price change, et comment le moteur suit ce qui se passe sur la page. Ce que vous pouvez observer ici ne ressemble pas à une application JS standard.
Ce n'est peut-être pas encore évident, mais le principal problème que nous devons résoudre ici est que les programmes JS ne fonctionnent généralement pas comme ça. Par exemple, exécutons le code suivant:
 let price = 5 let quantity = 2 let total = price * quantity  
Que pensez-vous qui sera affiché dans la console? Étant donné que rien n'est utilisé ici, sauf JS ordinaire, 
10 arrivera à la console.
Le résultat du programmeEt lorsque vous utilisez les capacités de Vue, dans une situation similaire, nous pouvons implémenter un scénario dans lequel la valeur 
total est recomptée lorsque les variables de 
price ou de 
quantity changent. Autrement dit, si le système de réactivité était utilisé dans l'exécution du code ci-dessus, alors pas 10, mais 40 serait affiché sur la console:
Sortie console générée par un code hypothétique utilisant un système de réactivitéJavaScript est un langage qui peut fonctionner à la fois procédural et orienté objet, mais il n'a pas de système de réactivité intégré, donc le code que nous avons pris en compte lors du changement de 
price ne donnera pas le numéro 40 à la console. Pour que l'indicateur 
total soit recalculé lorsque le 
price ou la 
quantity change, nous devrons créer nous-mêmes un système de réactivité et ainsi obtenir le comportement dont nous avons besoin. Nous allons briser le chemin vers cet objectif en plusieurs petites étapes.
Tâche: stockage des règles de calcul des indicateurs
Nous avons besoin d'un endroit pour enregistrer des informations sur la façon dont l'indicateur 
total est calculé, ce qui nous permettra de le recalculer lors du changement des valeurs des variables de 
price ou de 
quantity .
▍Solution
Tout d'abord, nous devons dire à l'application ce qui suit: "Voici le code que je vais exécuter, enregistrez-le, je devrai peut-être l'exécuter une autre fois." Ensuite, nous devrons exécuter le code. Plus tard, si les indicateurs de 
price ou de 
quantity ont changé, vous devrez appeler le code enregistré pour recalculer le 
total . Cela ressemble à ceci:
Le code de calcul total doit être enregistré quelque part afin de pouvoir y accéder plus tardLe code que vous pouvez appeler en JavaScript pour effectuer une action est formaté en tant que fonctions. Par conséquent, nous écrirons une fonction qui traite du calcul du 
total , et créerons également un mécanisme pour stocker les fonctions dont nous pourrions avoir besoin plus tard.
 let price = 5 let quantity = 2 let total = 0 let target = null target = function () {   total = price * quantity } record()  
Notez que nous stockons la fonction anonyme dans la variable 
target , puis appelons la fonction d' 
record . Nous en parlerons ci-dessous. Je voudrais également noter que la fonction 
target , en utilisant la syntaxe des fonctions fléchées ES6, peut être réécrite comme suit:
 target = () => { total = price * quantity } 
Voici la déclaration de la fonction d' 
record et la structure de données utilisée pour stocker les fonctions:
 let storage = [] //     target function record () { // target = () => { total = price * quantity }   storage.push(target) } 
En utilisant la fonction d' 
record , nous enregistrons la fonction 
target (dans notre cas 
{ total = price * quantity } ) dans la matrice de 
storage , ce qui nous permet d'appeler cette fonction plus tard, éventuellement en utilisant la fonction de 
replay , dont le code est illustré ci-dessous. Cela nous permettra d'appeler toutes les fonctions stockées dans le 
storage .
 function replay () {   storage.forEach(run => run()) } 
Ici, nous passons en revue toutes les fonctions anonymes stockées dans la matrice de 
storage et exécutons chacune d'entre elles.
Ensuite, dans notre code, nous pouvons faire ce qui suit:
 price = 20 console.log(total) // 10 replay() console.log(total) // 40 
Ça n'a pas l'air si difficile, non? Voici l'intégralité du code, dont nous avons discuté les fragments ci-dessus, au cas où il serait plus pratique pour vous de finalement y faire face. Soit dit en passant, ce code n'est pas accidentellement écrit de cette façon.
 let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () {   storage.push(target) } function replay () {   storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total)  
C'est ce qui sera affiché dans la console du navigateur après son démarrage.
Résultat du codeChallenge: une solution fiable pour stocker des fonctions
Nous pouvons continuer à noter les fonctions dont nous avons besoin si nécessaire, mais ce serait bien si nous avions une solution plus fiable qui peut être mise à l'échelle avec l'application. Ce sera peut-être une classe qui conserve une liste de fonctions écrites à l'origine dans la variable 
target et qui reçoit des notifications si nous devons réexécuter ces fonctions.
▍Solution: classe de dépendance
Une approche pour résoudre le problème ci-dessus consiste à encapsuler le comportement dont nous avons besoin dans une classe, qui peut être appelée dépendance. Cette classe implémentera le modèle de programmation d'observateur standard.
Par conséquent, si nous créons une classe JS utilisée pour gérer nos dépendances (qui sera proche de la façon dont des mécanismes similaires sont implémentés dans Vue), cela peut ressembler à ceci:
 class Dep { // Dep -    Dependency   constructor () {       this.subscribers = [] //  ,                               //    notify()   }   depend () { //   record       if (target && !this.subscribers.includes(target)){           //    target                //                this.subscribers.push(target)       }   }   notify () { //   replay       this.subscribers.forEach(sub => sub())       //  -     } } 
Veuillez noter qu'au lieu de la matrice de 
storage , nous stockons maintenant nos fonctions anonymes dans la matrice des 
subscribers . Au lieu de la fonction d' 
record , la méthode 
depend est maintenant appelée. Ici aussi, au lieu de la fonction de 
replay , la fonction de 
notify est 
notify . Voici comment exécuter notre code à l'aide de la classe 
Dep :
 const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend()  
Notre nouveau code fonctionne de la même manière qu'auparavant, mais maintenant il est mieux conçu et il semble préférable de le réutiliser.
Jusqu'à présent, la seule chose qui semble étrange est de travailler avec une fonction stockée dans la variable 
target .
Tâche: mécanisme de création de fonctions anonymes
À l'avenir, nous devrons créer un objet de classe 
Dep pour chaque variable. De plus, il serait bien d'encapsuler le comportement de création de fonctions anonymes quelque part, qui devrait être appelé lors de la mise à jour des données pertinentes. Peut-être que cela nous aidera avec une fonction supplémentaire, que nous appellerons 
watcher . Cela conduira au fait que nous pouvons remplacer cette construction de l'exemple précédent par une nouvelle fonction:
 let target = () => { total = price * quantity } dep.depend() target() 
En fait, un appel à la fonction 
watcher qui remplace ce code ressemblera à ceci:
 watcher(() => {   total = price * quantity }) 
▍ Solution: fonction d'observateur
A l'intérieur de la fonction 
watcher , dont le code est présenté ci-dessous, nous pouvons effectuer plusieurs actions simples:
 function watcher(myFunc) {   target = myFunc //   target   myFunc   dep.depend() //  target      target() //     target = null //   target } 
Comme vous pouvez le voir, la fonction 
watcher prend, comme argument, la fonction 
myFunc , l'écrit dans la variable 
target globale, appelle 
dep.depend() pour ajouter cette fonction à la liste des abonnés, appelle cette fonction et réinitialise la variable 
target .
Maintenant, nous obtenons tous les mêmes valeurs 10 et 40 si nous exécutons le code suivant:
 price = 20 console.log(total) dep.notify() console.log(total) 
Vous vous demandez peut-être pourquoi nous avons implémenté 
target tant que variable globale, au lieu de transmettre cette variable à nos fonctions, si nécessaire. Nous avons de bonnes raisons de le faire, vous comprendrez plus tard.
Tâche: posséder un objet Dep pour chaque variable
Nous avons un seul objet de classe 
Dep . Et si nous avions besoin que chacune de nos variables ait son propre objet de classe 
Dep ? Avant de continuer, déplaçons les données avec lesquelles nous travaillons vers les propriétés de l'objet:
 let data = { price: 5, quantity: 2 } 
Imaginez un instant que chacune de nos propriétés ( 
price et 
quantity ) possède son propre objet de classe 
Dep interne.
Propriétés de prix et de quantitéMaintenant, nous pouvons appeler la fonction 
watcher comme ceci:
 watcher(() => {   total = data.price * data.quantity }) 
Puisque nous travaillons ici avec la valeur de la propriété 
data.price , nous avons besoin de l'objet de classe 
Dep de la propriété 
price pour placer une fonction anonyme (stockée dans 
target ) dans son tableau d'abonnés (en appelant 
dep.depend() ). De plus, puisque nous travaillons avec 
data.quantity , nous avons besoin de l'objet 
Dep de la propriété 
quantity pour placer une fonction anonyme (à nouveau, stockée dans 
target ) dans son tableau d'abonnés.
Si vous décrivez cela sous la forme d'un diagramme, vous obtenez ce qui suit.
Les fonctions tombent dans des tableaux d'abonnés d'objets de classe Dep correspondant à différentes propriétésSi nous avons une fonction anonyme de plus dans laquelle nous travaillons uniquement avec la propriété 
data.price , alors la fonction anonyme correspondante ne doit aller qu'au dépôt de l'objet de cette propriété.
Des observateurs supplémentaires peuvent être ajoutés à une seule des propriétés disponibles.Quand pourriez-vous avoir besoin d'appeler 
dep.notify() pour les fonctions souscrites aux modifications de la propriété 
price ? Cela sera nécessaire lors du changement de 
price . Cela signifie que lorsque notre exemple est complètement prêt, le code suivant devrait fonctionner pour nous.
Ici, lors du changement de prix, vous devez appeler dep.notify () pour toutes les fonctions souscrites au changement de prixPour que tout fonctionne de cette façon, nous avons besoin d'un moyen d'intercepter les événements d'accès à la propriété (dans notre cas, c'est le 
price ou la 
quantity ). Cela permettra, lorsque cela se produira, de sauvegarder la fonction 
target dans un tableau d'abonnés, et lorsque la variable correspondante changera, d'exécuter la fonction stockée dans ce tableau.
▍Solution: Object.defineProperty ()
Maintenant, nous devons nous familiariser avec la méthode ES5 standard 
Object.defineProperty (). Il vous permet d'affecter des getters et setters aux propriétés des objets. Permettez-moi, avant de passer à leur utilisation pratique, de démontrer le fonctionnement de ces mécanismes avec un exemple simple.
 let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { //       price   get() { //        console.log(`I was accessed`)   },   set(newVal) { //        console.log(`I was changed`)   } }) data.price //       data.price = 20 //      
Si vous exécutez ce code dans la console du navigateur, il affichera le texte suivant.
Getter and Setter ResultsComme vous pouvez le voir, notre exemple imprime simplement quelques lignes de texte sur la console. Cependant, il ne lit ni ne définit de valeurs, car nous avons redéfini la fonctionnalité standard des getters et setters. Nous restaurerons la fonctionnalité de ces méthodes. À savoir, il est prévu que les getters retournent les valeurs des méthodes correspondantes et que les setters les définissent. Par conséquent, nous ajouterons une nouvelle variable, 
internalValue , au code, que nous utiliserons pour stocker la valeur actuelle du 
price .
 let data = { price: 5, quantity: 2 } let internalValue = data.price //   Object.defineProperty(data, 'price', { //       price   get() { //        console.log(`Getting price: ${internalValue}`)       return internalValue   },   set(newVal) {       console.log(`Setting price to: ${newVal}`)       internalValue = newVal   } }) total = data.price * data.quantity //       data.price = 20 //      
Maintenant que le getter et le setter fonctionnent comme ils devraient fonctionner, que pensez-vous qu'il y aura dans la console lorsque ce code sera exécuté? Jetez un œil à la figure suivante.
Sortie de données vers la consoleDonc, nous avons maintenant un mécanisme qui vous permet de recevoir des notifications lors de la lecture des valeurs de propriété et lorsque de nouvelles valeurs leur sont écrites. Maintenant, après avoir un peu retravaillé le code, nous pouvons équiper les getters et les setters de toutes les propriétés de l'objet de 
data . Ici, nous utiliserons la méthode 
Object.keys() , qui retourne un tableau de clés de l'objet qui lui est passé.
 let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { //        data   let internalValue = data[key]   Object.defineProperty(data, key, {       get() {           console.log(`Getting ${key}: ${internalValue}`)           return internalValue       },       set(newVal) {           console.log(`Setting ${key} to: ${newVal}`)           internalValue = newVal       }   }) }) let total = data.price * data.quantity data.price = 20 
Maintenant, toutes les propriétés de l'objet de 
data ont des getters et des setters. C'est ce qui apparaît dans la console après avoir exécuté ce code.
Sortie de données vers la console par les getters et les settersAssemblage du système de réactivité
Lorsqu'un fragment de code comme 
total = data.price * data.quantity et 
total = data.price * data.quantity valeur de la propriété 
price , nous avons besoin de la propriété 
price pour «se souvenir» de la fonction anonyme correspondante ( 
target dans notre cas). Par conséquent, si la propriété 
price est modifiée, c'est-à-dire définie sur une nouvelle valeur, cela conduira à un appel à cette fonction pour répéter les opérations qu'elle effectue, car elle sait qu'une certaine ligne de code en dépend. En conséquence, les opérations effectuées dans les getters et setters peuvent être imaginées comme suit:
- Getter - vous devez vous souvenir de la fonction anonyme, que nous appellerons à nouveau lorsque la valeur changera.
- Setter - il est nécessaire d'exécuter la fonction anonyme stockée, ce qui entraînera une modification de la valeur résultante correspondante.
Si vous utilisez la classe 
Dep déjà connue dans cette description, vous obtenez les éléments suivants:
- Lors de la lecture d'une valeur de propriété, dep.depend()est appelé pour enregistrer la fonctiontargetactuelle.
- Lorsqu'une valeur est écrite dans une propriété, dep.notify()est appelé pour redémarrer toutes les fonctions stockées.
Maintenant, nous allons combiner ces deux idées et, enfin, nous arriverons au code qui nous permet d'atteindre notre objectif.
 let data = { price: 5, quantity: 2 } let target = null //  -    ,     class Dep {   constructor () {       this.subscribers = []   }   depend () {       if (target && !this.subscribers.includes(target)){           this.subscribers.push(target)       }   }   notify () {       this.subscribers.forEach(sub => sub())   } } //      ,  //      Object.keys(data).forEach(key => {   let internalValue = data[key]   //         //   Dep   const dep = new Dep()   Object.defineProperty(data, key, {       get() {           dep.depend() //    target           return internalValue       },       set(newVal) {           internalValue = newVal           dep.notify() //           }   }) }) //   watcher   dep.depend(), //        function watcher(myFunc){   target = myFunc   target()   target = null } watcher(() => {   data.total = data.price * data.quantity }) 
Essayons avec ce code dans la console du navigateur.
Expériences de code prêtComme vous pouvez le voir, cela fonctionne exactement comme nous en avons besoin! Les propriétés de 
price et de 
quantity sont devenues réactives! Tout le code responsable de la génération du 
total lorsque le 
price ou la 
quantity change est exécuté à plusieurs reprises.
Maintenant, après avoir écrit notre propre système de réactivité, cette illustration de la documentation Vue vous semblera familière et compréhensible.
Système de réactivité VueVoir ce beau cercle violet qui dit 
contenant des getters et setters? Maintenant, il devrait vous être familier. Chaque instance du composant a une instance de la méthode observateur (cercle bleu), qui collecte les dépendances sur les getters (ligne rouge). Lorsque, plus tard, le setter est appelé, il notifie la méthode observateur, ce qui conduit au re-rendu du composant. Voici le même schéma, fourni avec des explications le reliant à notre développement.
Diagramme de réactivité Vue avec explicationsNous pensons que maintenant, après avoir écrit notre propre système de réactivité, ce schéma n'a pas besoin d'explications supplémentaires.
Bien sûr, dans Vue, tout cela est plus compliqué, mais maintenant vous devez comprendre le mécanisme sous-jacent aux systèmes de réactivité.
Résumé
Après avoir lu ce matériel, vous avez appris ce qui suit:
- Comment créer une classe Depqui collecte des fonctions à l'aide de la méthodedependet, si nécessaire, les appelle à nouveau à l'aide de la méthodenotify.
- Comment créer une fonction d' watcherqui vous permet de contrôler le code que nous exécutons (c'est la fonctiontarget), que vous devrez peut-être enregistrer dans l'objet de la classeDep.
- Comment utiliser la méthode Object.defineProperty()pour créer des getters et des setters.
Tout cela, compilé dans un seul exemple, a conduit à la création d'un système de réactivité en JavaScript pur, en comprenant que vous pouvez comprendre les caractéristiques du fonctionnement de tels systèmes utilisés dans les frameworks web modernes.
Chers lecteurs! Si, avant de lire ce matériel, vous avez mal imaginé les caractéristiques des mécanismes des systèmes de réactivité, dites-moi, avez-vous maintenant réussi à y faire face?