Révolutions de la liaison de données avec Object.observe()

Addy Osmani
Addy Osmani

Introduction

Une révolution est en marche. Un nouvel ajout à JavaScript va modifier tout ce que vous savez sur la liaison de données. Cela va également modifier la façon dont de nombreuses bibliothèques MVC abordent l'observation des modèles pour les modifications et les mises à jour. Prêt à améliorer les performances des applications qui se soucient de l'observation des propriétés ?

D\'accord. Sans plus attendre, je suis heureux de vous annoncer que Object.observe() est disponible dans la version stable de Chrome 36. [WOOOO. LA FOULE EST EN LIESSE].

Object.observe(), qui fait partie d'une future norme ECMAScript, est une méthode permettant d'observer de manière asynchrone les modifications apportées aux objets JavaScript, sans avoir besoin d'une bibliothèque distincte. Il permet à un observateur de recevoir une séquence ordonnée par date et heure d'enregistrements de modifications qui décrivent l'ensemble des modifications apportées à un ensemble d'objets observés.

// Let's say we have a model with data
var model = {};

// Which we then observe
Object.observe(model, function(changes){

    // This asynchronous callback runs
    changes.forEach(function(change) {

        // Letting us know what changed
        console.log(change.type, change.name, change.oldValue);
    });

});

Chaque fois qu'une modification est effectuée, elle est signalée:

Modification signalée.

Avec Object.observe() (j'aime l'appeler O.o() ou Oooooooo), vous pouvez implémenter la liaison de données bidirectionnelle sans avoir besoin d'un framework.

Cela ne veut pas dire que vous ne devriez pas en utiliser un. Pour les grands projets comportant une logique métier complexe, les frameworks orientés sont inestimables et vous devez continuer à les utiliser. Ils simplifient l'orientation des nouveaux développeurs, nécessitent moins de maintenance du code et imposent des modèles pour accomplir des tâches courantes. Lorsque vous n'en avez pas besoin, vous pouvez utiliser des bibliothèques plus petites et plus ciblées, comme Polymer (qui exploite déjà O.o()).

Même si vous utilisez beaucoup un framework ou une bibliothèque MV*, O.o() peut leur apporter des améliorations de performances intéressantes, avec une implémentation plus rapide et plus simple, tout en conservant la même API. Par exemple, l'année dernière, Angular a constaté que dans un benchmark où des modifications étaient apportées à un modèle, la vérification des erreurs prenait 40 ms par mise à jour et O.o() de 1 à 2 ms par mise à jour (soit 20 à 40 fois plus rapide).

Avec la liaison de données, vous n'avez pas besoin d'encombrer de code compliqué, vous n'avez plus à vous soucier des modifications et vous bénéficiez d'une autonomie de batterie plus longue !

Si vous êtes déjà convaincu par O.o(), passez à la présentation de la fonctionnalité ou lisez la suite pour en savoir plus sur les problèmes qu'elle résout.

Que voulons-nous observer ?

Lorsque nous parlons d'observation des données, nous faisons généralement référence à la surveillance de certains types de changements spécifiques:

  • Modifications apportées aux objets JavaScript bruts
  • Lorsque des propriétés sont ajoutées, modifiées ou supprimées
  • Lorsque les tableaux comportent des éléments collés et en dehors
  • Modifications apportées au prototype de l'objet

L'importance de la liaison de données

La liaison de données commence à devenir importante lorsque vous vous souciez de la séparation du contrôle modèle-vue. HTML est un excellent mécanisme déclaratif, mais il est entièrement statique. Idéalement, vous souhaitez simplement déclarer la relation entre vos données et le DOM, et mettre à jour le DOM. Vous gagnez ainsi beaucoup de temps en rédigeant du code vraiment répétitif, qui se contente d'envoyer des données depuis et vers le DOM entre l'état interne de votre application ou le serveur.

La liaison de données est particulièrement utile lorsque vous disposez d'une interface utilisateur complexe dans laquelle vous devez établir des relations entre plusieurs propriétés de vos modèles de données et plusieurs éléments de vos vues. Cela est assez courant dans les applications monopages que nous créons aujourd'hui.

En implémentant un moyen d'observer les données de manière native dans le navigateur, nous donnons aux frameworks JavaScript (et aux petites bibliothèques d'utilitaires que vous écrivez) un moyen d'observer les modifications apportées aux données de modèle sans s'appuyer sur certains des hacks lents utilisés dans le monde aujourd'hui.

À quoi ressemble le monde d'aujourd'hui

Vérification des saletés

Où avez-vous déjà vu la liaison de données ? Si vous utilisez une bibliothèque MV* moderne pour créer vos applications Web (Angular, Knockout, par exemple), vous êtes probablement habitué à lier les données du modèle au DOM. Pour rappel, voici un exemple d'application de liste de téléphones dans laquelle nous liant la valeur de chaque téléphone dans un tableau phones (défini en JavaScript) à un élément de liste afin que nos données et notre UI soient toujours synchronisées :

<html ng-app>
  <head>
    ...
    <script src='angular.js'></script>
    <script src='controller.js'></script>
  </head>
  <body ng-controller='PhoneListCtrl'>
    <ul>
      <li ng-repeat='phone in phones'>
        
        <p></p>
      </li>
    </ul>
  </body>
</html>

et le code JavaScript du contrôleur :

var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.'},
    {'name': 'Motorola XOOM with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.'},
    {'name': 'MOTOROLA XOOM',
     'snippet': 'The Next, Next Generation tablet.'}
  ];
});

Chaque fois que les données du modèle sous-jacent changent, notre liste dans le DOM est mise à jour. Comment Angular y parvient-il ? En arrière-plan, il effectue une vérification des saletés.

Vérifications sales

L'idée de base avec la vérification des saletés est que chaque fois que les données auraient pu changer, la bibliothèque doit vérifier si elle a changé via un condensat ou un cycle de changement. Dans le cas d'Angular, un cycle de récapitulatif identifie toutes les expressions enregistrées pour être surveillées afin de voir s'il y a un changement. Il connaît les valeurs précédentes d'un modèle et, si elles ont changé, un événement de modification est déclenché. Pour un développeur, l'avantage principal est que vous pouvez utiliser des données d'objet JavaScript brutes, qui sont agréables à utiliser et se composent assez bien. L'inconvénient est qu'il présente un mauvais comportement algorithmique et qu'il est potentiellement très coûteux.

Mauvaise vérification.

Le coût de cette opération est proportionnel au nombre total d'objets observés. Je devrai peut-être effectuer de nombreuses vérifications de saleté. Peut également nécessiter un moyen de déclencher un contrôle de mauvaise qualité lorsque les données peuvent avoir changé. Les frameworks utilisent de nombreuses astuces pour ce faire. Il n'est pas clair si cela sera jamais parfait.

L'écosystème Web doit être plus en mesure d'innover et d'évoluer ses propres mécanismes déclaratifs, par exemple :

  • Systèmes de modèles basés sur des contraintes
  • Systèmes de persistance automatique (par exemple, modifications persistantes apportées à IndexedDB ou localStorage)
  • Objets de conteneur (Ember, Backbone)

Les objets Container permettent à un framework de créer des objets qui, à l'intérieur, contiennent les données. Ils disposent d'accesseurs aux données et peuvent capturer ce que vous définissez ou obtenez, et diffuser en interne. Cela fonctionne bien. Il est relativement performant et présente un bon comportement algorithmique. Vous trouverez ci-dessous un exemple d'objets de conteneur utilisant Ember :

// Container objects
MyApp.president = Ember.Object.create({
  name: "Barack Obama"
});
 
MyApp.country = Ember.Object.create({
  // ending a property with "Binding" tells Ember to
  // create a binding to the presidentName property
  presidentNameBinding: "MyApp.president.name"
});
 
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
 
// Data from the server needs to be converted
// Composes poorly with existing code

La dépense liée à la découverte de ce qui a changé ici est proportionnelle au nombre de choses qui ont changé. Un autre problème est que vous utilisez maintenant ce type d'objet différent. En règle générale, vous devez convertir les données transmises par le serveur vers ces objets afin qu'ils soient observables.

Cela ne se compose pas particulièrement bien avec le code JS existant, car la plupart du code suppose qu'il peut fonctionner sur des données brutes. Pas pour ces types d'objets spécialisés.

Introducing Object.observe()

Dans l'idéal, ce que nous voulons, c'est le meilleur des deux mondes : un moyen d'observer les données avec prise en charge d'objets de données brutes (objets JavaScript standards) si nous choisissons d'utiliser AND sans avoir à tout vérifier en permanence. Un élément avec un bon comportement algorithmique. Quelque chose qui est bien composé et intégré à la plate-forme. C'est toute la beauté de Object.observe().

Elle nous permet d'observer un objet, de modifier les propriétés et de consulter le rapport des modifications sur ce qui a changé. Mais assez de théorie. Passons au code !

Object.observe()

Object.observe() et Object.unobserve()

Imaginons que nous disposions d'un simple objet JavaScript représentant un modèle :

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

Nous pouvons ensuite spécifier un rappel chaque fois que des mutations (modifications) sont apportées à l'objet :

function observer(changes){
  changes.forEach(function(change, i){
      console.log('what property changed? ' + change.name);
      console.log('how did it change? ' + change.type);
      console.log('whats the current value? ' + change.object[change.name]);
      console.log(change); // all changes
  });
}

Nous pouvons ensuite observer ces modifications à l'aide de O.o(), en transmettant l'objet comme premier argument et le rappel comme deuxième argument :

Object.observe(todoModel, observer);

Commençons par apporter des modifications à notre objet de modèle Todos :

todoModel.label = 'Buy some more milk';

En examinant la console, nous obtenons des informations utiles. Nous savons quelle propriété a changé, comment elle a été modifiée et quelle est la nouvelle valeur.

Rapport de la console

Bravo ! Adieu, vérification de l'état de modification ! Votre pierre tombale doit être sculptée en Comic Sans. Modifions une autre propriété. Cette fois, completeBy:

todoModel.completeBy = '01/01/2014';

Comme nous pouvons le constater, nous obtenons à nouveau un rapport de modification :

Rapport sur les modifications.

Parfait. Et si nous décidions à présent de supprimer la propriété "completed" de notre objet:

delete todoModel.completed;
Terminé

Comme vous pouvez le constater, le rapport sur les modifications renvoyées inclut des informations sur la suppression. Comme prévu, la nouvelle valeur de la propriété est désormais indéfinie. Vous savez maintenant comment savoir quand des établissements ont été ajoutés. Lorsqu'ils ont été supprimés. En gros, il s'agit de l'ensemble de propriétés d'un objet ("nouveau", "supprimé" ou "reconfiguré") et de la modification de son prototype (proto).

Comme dans tout système d'observation, une méthode permet également d'arrêter d'écouter les changements. Dans ce cas, il s'agit de Object.unobserve(), qui a la même signature que O.o() mais peut être appelé comme suit :

Object.unobserve(todoModel, observer);

Comme vous pouvez le voir ci-dessous, les éventuelles mutations apportées à l'objet après son exécution n'entraînent plus le renvoi d'une liste d'enregistrements de modifications.

Mutations

Spécifier les modifications d'intérêt

Nous avons donc vu les bases pour récupérer une liste de modifications apportées à un objet observé. Que se passe-t-il si vous vous intéressez uniquement à un sous-ensemble des modifications apportées à un objet, et non à toutes les modifications ? Tout le monde a besoin d'un filtre antispam. Les observateurs ne peuvent spécifier que les types de modifications dont ils souhaitent être informés via une liste d'acceptation. Vous pouvez le spécifier à l'aide du troisième argument de O.o() comme suit :

Object.observe(obj, callback, optAcceptList)

Prenons un exemple d'utilisation:

// Like earlier, a model can be a simple vanilla object

var todoModel = {
  label: 'Default',
  completed: false

};


// We then specify a callback for whenever mutations 
// are made to the object
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })

};

// Which we then observe, specifying an array of change 
// types we're interested in

Object.observe(todoModel, observer, ['delete']);

// without this third option, the change types provided 
// default to intrinsic types

todoModel.label = 'Buy some milk'; 

// note that no changes were reported

Si nous supprimons maintenant le libellé, notez que ce type de modification est signalé :

delete todoModel.label;

Si vous ne spécifiez pas de liste de types d'acceptation sur O.o(), les types de modifications d'objets "intrinsiques" sont définis par défaut (add, update, delete, reconfigure, preventExtensions (pour les cas où un objet devenir non extensible n'est pas observable)).

Notifications

O.o() s’accompagne également de la notion de notifications. Ils ne ressemblent en rien à ces éléments ennuyeux que vous trouvez sur un téléphone, mais sont plutôt utiles. Les notifications sont semblables aux observateurs de mutation. Elles ont lieu à la fin de la micro-tâche. Dans le contexte du navigateur, elle se trouve presque toujours à la fin du gestionnaire d'événements actuel.

Le timing est idéal, car généralement, une unité de travail est terminée et les observateurs peuvent maintenant s'atteler à leur tâche. C'est un très bon modèle de traitement au tour par tour.

Le workflow d'utilisation d'un système d'alerte se présente comme suit:

Notifications

Voyons un exemple d'utilisation concrète des notificateurs pour définir des notifications personnalisées lorsque les propriétés d'un objet sont obtenues ou définies. Consultez les commentaires sur cette page :

// Define a simple model
var model = {
    a: {}
};

// And a separate variable we'll be using for our model's 
// getter in just a moment
var _b = 2;

// Define a new property 'b' under 'a' with a custom
// getter and setter

Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {

        // Whenever 'b' is set on the model
        // notify the world about a specific type
        // of change being made. This gives you a huge
        // amount of control over notifications
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });

        // Let's also log out the value anytime it gets
        // set for kicks
        console.log('set', b);

        _b = b;
    }
});

// Set up our observer
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}

// Begin observing model.a for changes
Object.observe(model.a, observer);
Console des notifications

Ici, nous indiquons quand la valeur des propriétés de données change ("mise à jour"). Tout autre élément que l'implémentation de l'objet choisit de signaler (notifier.notifyChange())

Nos années d'expérience sur la plate-forme Web nous ont appris qu'une approche synchrone est la première chose à essayer, car elle est la plus facile à comprendre. Le problème est qu'il crée un modèle de traitement fondamentalement dangereux. Si vous écrivez du code et que vous indiquez, par exemple, mettre à jour la propriété d'un objet, vous ne voulez pas vraiment que la mise à jour de la propriété de cet objet puisse inviter un code arbitraire à faire ce qu'il veut. Il n'est pas idéal que vos hypothèses soient invalidées au milieu d'une fonction.

Si vous êtes un observateur, vous ne voulez idéalement pas être appelé si quelqu'un se trouve au milieu de quelque chose. Vous ne voulez pas être invité à travailler sur un état du monde incohérent. Effectuer beaucoup plus de vérifications d'erreurs Essayer de tolérer beaucoup plus de situations dangereuses et, en général, c'est un modèle difficile à traiter. L'async est plus difficile à gérer, mais c'est un meilleur modèle au bout du compte.

La solution à ce problème est les enregistrements de modification synthétiques.

Enregistrements de modifications synthétiques

Fondamentalement, si vous souhaitez disposer d'accesseurs ou de propriétés calculées, vous devez en informer lorsque ces valeurs changent. Il s'agit d'un travail supplémentaire, mais il est conçu comme une sorte de fonctionnalité de premier ordre de ce mécanisme. Ces notifications seront distribuées avec le reste des notifications provenant des objets de données sous-jacents. À partir des propriétés des données

Enregistrements de modifications synthétiques

L'observation des accésseurs et des propriétés calculées peut être résolue avec notifier.notify, une autre partie d'O.o(). La plupart des systèmes d'observation nécessitent une forme d'observation des valeurs dérivées. Il existe de nombreuses façons de le faire. O.o ne fait aucun jugement sur la "bonne" méthode. Les propriétés calculées doivent correspondre à des accesseurs qui informent les changements d'état interne (privé).

Encore une fois, les développeurs Web doivent s'attendre à ce que les bibliothèques facilitent la notification et les différentes approches des propriétés calculées (et réduisent le code répétitif).

Configurez l'exemple suivant, qui est une classe de cercle. L'idée est que nous avons ce cercle et une propriété de rayon. Dans ce cas, le rayon est un accesseur. Lorsque sa valeur change, il s'en informe. Cette information sera envoyée avec toutes les autres modifications apportées à cet objet ou à tout autre objet. En gros, si vous implémentez un objet, vous devez choisir des propriétés synthétiques ou calculées, ou une stratégie pour que cela fonctionne. Une fois que vous l'aurez fait, il s'intégrera à l'ensemble de votre système.

Passez le code pour voir comment cela fonctionne dans les outils de développement.

function Circle(r) {
  var radius = r;
 
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
 
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
 
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
 
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}
Console des enregistrements de modifications synthétiques

Propriétés de l'accesseur

Petite remarque sur les propriétés des accesseurs. Nous avons mentionné précédemment que seules les modifications de valeur sont observables pour les propriétés de données. Non pour les propriétés calculées ni les accesseurs. En effet, JavaScript n'a pas vraiment la notion de modification de valeur des accésseurs. Un accesseur n'est qu'un ensemble de fonctions.

Si vous attribuez une fonction à un accesseur, JavaScript n'appelle que la fonction et, de son point de vue, rien n'a changé. Elle a permis à du code de s'exécuter.

Le problème est que, sémantiquement, nous pouvons examiner notre attribution ci-dessus à la valeur -5. Nous devrions pouvoir savoir ce qui s'est passé ici. Il s'agit en fait d'un problème insoluble. L'exemple montre pourquoi. Aucun système ne peut vraiment savoir ce que cela signifie, car il peut s'agir d'un code arbitraire. Dans ce cas, il peut faire ce qu'il veut. La valeur est mise à jour chaque fois qu'elle est consultée. Il n'a donc pas beaucoup de sens de demander si elle a changé.

Observer plusieurs objets avec un seul rappel

Un autre modèle possible avec O.o() est la notion d'un seul observateur de rappel. Cela permet d'utiliser un seul rappel en tant qu'"observateur" pour de nombreux objets différents. Le rappel envoie l'ensemble complet des modifications à tous les objets qu'il observe à la "fin de la microtâche" (notez la similitude avec les observateurs de mutation).

Observer plusieurs objets avec un seul rappel

Modifications à grande échelle

Vous travaillez peut-être sur une application vraiment importante et devez régulièrement gérer des changements à grande échelle. Les objets peuvent souhaiter décrire des modifications sémantiques plus importantes, ce qui affectera de nombreuses propriétés de manière plus compacte (au lieu de diffuser des tonnes de modifications de propriété).

O.o() vous aide à y parvenir grâce à deux utilitaires spécifiques : notifier.performChange() et notifier.notify(), que nous avons déjà présentés.

Modifications à grande échelle

Voyons comment décrire des changements à grande échelle en définissant un objet Thingy avec des utilitaires mathématiques (multiplier, incrémenter, incrémenterEtMultiplier). Chaque fois qu'un utilitaire est utilisé, il indique au système qu'un ensemble de travaux comprend un type de modification spécifique.

Par exemple : notifier.performChange('foo', performFooChangeFn);

function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}

Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';


Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);

    // Tell the system that a collection of work comprises 
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },

  multiply: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },

  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

Nous définissons ensuite deux observateurs pour notre objet: l'un est un fourre-tout pour les modifications et l'autre ne renvoie que sur les types d'acceptation spécifiques que nous avons définis (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY).

var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};

observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};

observer2.callback = function(r){
    console.log('Observer 2', r);
}


Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}

Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

Nous pouvons maintenant commencer à jouer avec ce code. Définissons une nouvelle chose:

var thingy = new Thingy(2, 4);

Observez-la, puis apportez des modifications. Trop amusant. Il y en a tellement !

// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
Modifications à grande échelle

Tout ce qui se trouve à l'intérieur de la fonction « performer » est considéré comme le travail d'une « grande modification ». Les observateurs qui acceptent ce « grand changement » ne recevront que l'enregistrement « grand changement ». Les observateurs qui ne les reçoivent pas recevront les modifications sous-jacentes résultant du travail réalisé par « exécuter la fonction ».

Observer des tableaux

Nous avons parlé pendant un certain temps de l'observation des modifications apportées aux objets, mais qu'en est-il des tableaux ? Excellente question. Lorsqu'un utilisateur me dit "Excellente question" Je n'entends jamais leur réponse parce que je suis occupé à me féliciter pour avoir posé une si excellente question, mais je m'éloigne. Nous avons également de nouvelles méthodes pour travailler avec des tableaux.

Array.observe() est une méthode qui traite les modifications à grande échelle sur elle-même (par exemple, splice, unshift ou tout autre élément qui modifie implicitement sa longueur) comme un enregistrement de modification "splice". En interne, il utilise notifier.performChange("splice",...).

Voici un exemple où nous observons un "tableau" de modèle et obtenons de même une liste de modifications en cas de modification des données sous-jacentes :

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;

Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});

model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';
Observer des tableaux

Performances

Pour comprendre l'impact des performances de calcul d'O.o() sur les performances de calcul, considérez-le comme un cache de lecture. De manière générale, un cache est un excellent choix lorsque (par ordre d'importance) :

  1. La fréquence des lectures domine la fréquence des écritures.
  2. Vous pouvez créer un cache qui échange la quantité constante de travail nécessaire aux écritures pour offrir de meilleures performances algorithmiques lors des lectures.
  3. Le ralentissement constant des écritures est acceptable.

O.o() est conçu pour des cas d'utilisation tels que 1).

La vérification de l'état nécessite de conserver une copie de toutes les données que vous observez. Cela signifie que vous encourez un coût de mémoire structurel pour la vérification des modifications que vous n'obtenez pas avec O.o(). La vérification des modifications, bien qu'elle soit une solution de secours acceptable, est également une abstraction fondamentalement inefficace qui peut créer une complexité inutile pour les applications.

Pourquoi ? La vérification de l'état des données doit s'exécuter chaque fois que les données peuvent avoir changé. Il n'existe tout simplement pas de méthode très efficace, et toute approche présente des inconvénients importants (par exemple, la vérification d'un intervalle d'interrogation peut entraîner des artefacts visuels et des conditions de concurrence entre les problèmes de code). Le contrôle des saletés nécessite également un registre mondial d’observateurs, ce qui crée des risques de fuite de mémoire et des coûts de suppression évités par O.o().

Examinons quelques chiffres.

Les tests d'analyse comparative ci-dessous (disponibles sur GitHub) nous permettent de comparer la vérification des erreurs et O.o(). Ils sont structurés sous forme de graphiques de taille et de nombre de mutations observées. En règle générale, les performances de la vérification des erreurs sont proportionnelles au nombre d'objets observés, tandis que les performances de la fonction de vérification des erreurs sont proportionnelles au nombre de mutations effectuées.

Vérification de l'état de modification

Performances de la vérification de la propreté

Chrome avec Object.observe() activé

Observer les performances

Polyfilling Object.observe()

Parfait. O.o() peut être utilisé dans Chrome 36. Mais qu'en est-il dans d'autres navigateurs ? Nous sommes là pour vous aider. Observe-JS de Polymer est un polyfill pour O.o() qui utilise l'implémentation native si elle est présente, mais sinon la polyfille et inclut des sucreries utiles en plus. Elle offre une vue globale du monde qui résume les changements et fournit un rapport sur ce qui a changé. Il expose deux choses vraiment importantes:

  1. Vous pouvez observer les chemins. Cela signifie que vous pouvez dire "Je souhaite observer "foo.bar.baz" à partir d'un objet donné", et ils vous indiqueront quand la valeur de ce chemin a changé. Si le chemin est inaccessible, la valeur est considérée comme non définie.

Exemple d'observation d'une valeur sur un chemin d'accès à partir d'un objet donné:

var obj = { foo: { bar: 'baz' } };

var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
  // respond to obj.foo.bar having changed value.
});
  1. Il vous fournira des informations sur les raccordements de tableaux. Les épissures de tableau sont essentiellement l'ensemble minimal d'opérations d'épissure que vous devrez effectuer sur un tableau pour transformer l'ancienne version du tableau en nouvelle version. Il s'agit d'un type de transformation ou d'une vue différente du tableau. Il s'agit du travail minimal que vous devez effectuer pour passer de l'ancien état au nouvel état.

Exemple de rapport sur les modifications apportées à un tableau en tant qu'ensemble minimal d'épissures :

var arr = [0, 1, 2, 4];

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

Frameworks et Object.observe()

Comme mentionné précédemment, O.o() offrira aux frameworks et aux bibliothèques une énorme opportunité d'améliorer les performances de leur liaison de données dans les navigateurs compatibles avec cette fonctionnalité.

Yehuda Katz et Erik Bryn d'Ember ont confirmé que l'ajout de la compatibilité avec O.o() figure dans la feuille de route à court terme d'Ember. Misko Hervy a rédigé un document de conception sur la détection améliorée des modifications d'Angular 2.0. L'approche à long terme consistera à exploiter Object.observe() lorsqu'il sera disponible dans la version stable de Chrome et à opter pour Watchtower.js, sa propre méthode de détection des modifications en attendant. C'est super excitant.

Conclusions

O.o() est un ajout puissant à la plate-forme Web que vous pouvez utiliser dès aujourd'hui.

Nous espérons qu'à terme, cette fonctionnalité sera disponible dans davantage de navigateurs, ce qui permettra aux frameworks JavaScript d'améliorer les performances grâce à l'accès aux fonctionnalités natives d'observation des objets. Les utilisateurs qui ciblent Chrome devraient pouvoir utiliser O.o() dans Chrome 36 (et versions ultérieures). Cette fonctionnalité devrait également être disponible dans une prochaine version d'Opera.

Vous pouvez discuter avec les auteurs des frameworks JavaScript de Object.observe() et de la façon dont ils prévoient de l'utiliser pour améliorer les performances de la liaison de données dans vos applications. Il y a des moments vraiment passionnants à venir !

Ressources

Merci à Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan et Vivian Cromwell pour leurs commentaires et leurs commentaires.