Passer au contenu principal




En este capítulo, explicaré cómo crear una biblioteca de relier de datos simple pero potente con los nuevos proxy de ES6.

Pré-requis

ES6 hizo que JavaScript fuera mucho más elegante, pero la mayoría de las nuevas características son sólo sintácticas. Los proxies son una de las pocas adiciones no rellenables. Si no está familiarizado con ellos, por favor, eche un vistazo a los documentos del MDN Proxy antes de continuar.

También será útil tener un conocimiento básico de los objetos ES6 Reflection API y Set, Map y WeakMap.

La bibliothèque nx-observe

nx-observe est une solution de liaison de données en moins de 140 lignes de code. Expose les fonctions observable(obj) et observe(fn), qui sont utilisées pour créer des objets observables et des fonctions d'observation. Une fonction d'observation est automatiquement exécutée lorsqu'une propriété observable qu'elle utilise change. L'exemple suivant le démontre.

// ceci est un exemple observable const person = observable({name: 'John', age: 20}) function print () { console.log(`${person.name}, ${person.age}`) } // cela crée une fonction d'observation // affiche 'John, 20' sur la console observer(print) // affiche sur la console 'Dave, 20' setTimeout(() => person.name = 'Dave', 100) / /console sortie 'Dave, 22' setTimeout(() => personne.age = 22, 200)

La fonction d'impression passée à observe() est ré-exécutée chaque fois que person.name ou person.age change, print appelle à nouveau une fonction d'observation.

Implémentation d'un observable simple.

Dans cette section, je vais vous expliquer ce qui se passe sous le capot de nx-observe. Tout d'abord, je vais vous montrer comment les modifications apportées aux propriétés d'un observable sont détectées et combinées avec les observateurs. J'expliquerai ensuite une manière d'exécuter les fonctions d'observation déclenchées par ces changements.

Journal des modifications

Les modifications sont enregistrées en enveloppant des objets observables dans des proxys ES6. Ces proxys interceptent de manière transparente les opérations d'obtention et de définition à l'aide de l'API Reflection.

variables Observateuractuel et queueObserver() sont utilisés dans le code ci-dessous, mais ne seront expliqués que dans la section suivante. Pour l'instant, il suffit de savoir que currentObserver pointe toujours vers la fonction d'observation en cours d'exécution, et que queueObserver() est une fonction qui met en file d'attente un observateur à exécuter bientôt.

/* mappe les propriétés observables à un ensemble de fonctions d'observation, qui utilisent la propriété */ const observers = new WeakMap() /* pointe vers la fonction d'observation en cours d'exécution, peut être indéfinie */ let currentObserver /* rend un objet observable en enveloppant dans un proxy, ajoute également une carte vierge pour les paires propriété-observateur à enregistrer ultérieurement. */ function obj (obj) { observers.set(obj, new Map()) return new Proxy(obj, {get, set}) } /* ce trap intercepte les opérations, ne fait rien si aucun observateur ne s'exécute à ce moment. */ function get (target, key, receiver) { const result = Reflect.get(target, key, receiver) if (currentObserver) { registerObserver(target, key, currentObserver) } return result } /* si une fonction exécute l'observateur , cette fonction fait correspondre la fonction d'observation à la propriété observable actuellement récupérée et l'enregistre dans la carte d'observation. */ function registerObserver (target, key, observer) { let observersForKey = observers.get(target).get(key) if (!observersForKey) { observersForKey = new Set() observers.get(target).set(key, observersForKey ) } observersForKey.add(observer) } /* ce piège intercepte les opérations d'ensemble, met en file d'attente chaque observateur associé à la propriété actuellement définie pour être exécuté ultérieurement */ function set (target, key, value, receiver) { const observersForKey = observers.get (target).get(key) if (observersForKey) { observersForKey.forEach(queueObserver) } return Reflect.set(target, key, value, receiver) }

La rampe gett ne fait rien si currentObserver n'est pas défini. Sinon, il fait correspondre les observateurs de propriétés observables obtenus et l'observateur en cours d'exécution et les enregistre sur la carte faible. Les observateurs sont stockés dans une propriété Set observable. Cela garantit qu'il n'y a pas de doublons.

La rampe sett consiste à récupérer tous les observateurs correspondant à la propriété observable modifiée et à les mettre en file d'attente pour une exécution ultérieure.

Vous pouvez trouver une figure et une description étape par étape expliquant l'exemple de code nx-observe ci-dessous.

Liaison de données JavaScript avec proxy es6 - exemple de code observable

  • L'objet observable par la personne est créé.
  • currentObserver est configuré pour imprimer.
  • l'impression démarre.
  • person.name est récupéré à l'intérieur de l'impression.
  • Proxy get trap est invoqué sur la personne
  • L'ensemble des observateurs appartenant au couple (personne, nom) est
  • récupéré par observers.get(person).get('name').
  • currentObserver(print) est ajouté à l'ensemble des observateurs.
  • L'étape 4-7 s'exécute à nouveau avec person.age.
  • ${person.name}, ${person.age} Imprime sur la console.
  • l'impression se termine.
  • currentObserver est défini sur undefined.
  • Un autre code commence à s'exécuter.
  • person.age est défini sur une nouvelle valeur (22).
  • La capture d'ensemble de proxy en personne est invoquée
  • L'ensemble des observateurs appartenant au couple (personne, âge) est
  • récupéré par observers.get(person).get('age').
  • Les observateurs dans l'ensemble d'observateurs (y compris l'impression)
  • sont mis en file d'attente pour exécution.
  • l'impression s'exécute à nouveau.

observateurs en cours d'exécution

Les observateurs en file d'attente s'exécutent de manière asynchrone dans un lot, ce qui améliore les performances. Lors de l'enregistrement, les observateurs sont ajoutés de manière synchrone à l'ensemble queuedObservers. L'ensemble ne peut pas contenir de doublons, donc la mise en file d'attente du même observateur plusieurs fois n'entraînera pas plusieurs exécutions. Si Set était vide auparavant, une nouvelle tâche est planifiée pour itérer et exécuter tous les observateurs en file d'attente après un certain temps.

/* contient les fonctions d'observation activées, qui devraient être exécutées bientôt */ const queuedObservers = new Set() /* pointe vers l'observateur actuel, peut être indéfini */ let currentObserver /* la fonction watch exposée */ function observe (fn) { queueObserver(fn) } /* ajouter l'observateur à la file d'attente et s'assurer que la file d'attente s'exécute bientôt */ function queueObserver (observer) { if (queuedObservers.size === 0) { Promise.resolve().then (runObservers) } queuedObservers.add(observer) } /* exécuter les observateurs en file d'attente, currentObserver est défini sur undefined à la fin */ function runObservers () { try { queuedObservers.forEach(runObserver) } enfin { currentObserver = undefined queuedObservers.clear () } } /* définit le global currentObserver sur observer, puis l'exécute */ function runObserver (observer) { currentObserver = observer observer() }

Le code ci-dessus garantit qu'à chaque fois qu'un observateur est exécuté, la variable globale currentObserver pointe vers lui. L'activation de currentObserver "active" les gett-ramps pour écouter et faire correspondre currentObserver à toutes les propriétés observables qu'il utilise lors de son exécution.

Construire un arbre observable dynamique

Jusqu'à présent, notre modèle fonctionne bien avec des structures de données à un seul niveau, mais il nous oblige à envelopper chaque nouvelle propriété à valeur d'objet dans une observable à la main. Par exemple, le code suivant ne fonctionnerait pas comme prévu.

const person = observable({data: {name: 'John'}}) function print() { console.log(person.data.name) } // sortie vers la console 'John' observe(print) // ne rien faire setTimeout (() => personne.données.nom = 'Dave', 100)

Pour que ce code fonctionne, nous aurions besoin de remplacer observable({data: {name: 'John'}}) par observable({data: observable({name: 'John'})}). Heureusement, nous pouvons éliminer cet inconvénient en modifiant un get.



function get (target, key, receiver) {
const result = Reflect.get(target, key, receiver)
if (currentObserver) {
registerObserver(target, key, currentObserver)
if (typeof result === 'object') {
const observableResult = observable(result)
Reflect.set(target, key, observableResult, receiver)
return observableResult
}
}
return result
}


La captura get anterior envuelve el valor devuelto en un proxy observable antes de devolverlo, en caso de que être un objeto. Esto también es perfecto desde el punto de vista del rendimiento, ya que los observables solo se crean cuando realmente los necesita un observador.