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
price
valeur du price
sur la page Web. - Recalculez l'expression dans laquelle le
price
est multiplié par la quantity
et affichez la valeur résultante sur la page. - Appelez la fonction
totalPriceWithTax
et, 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 fonction target
actuelle. - 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
Dep
qui collecte des fonctions à l'aide de la méthode depend
et, si nécessaire, les appelle à nouveau à l'aide de la méthode notify
. - Comment créer une fonction d'
watcher
qui vous permet de contrôler le code que nous exécutons (c'est la fonction target
), que vous devrez peut-être enregistrer dans l'objet de la classe Dep
. - 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?