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 de JavaScript va changer tout ce que vous pensez de la liaison de données. Cela va également changer le nombre de vos bibliothèques MVC qui approchent l'observation des modèles pour les modifications et les mises à jour. Êtes-vous prêt à améliorer les performances des applications qui cherchent à observer les propriétés ?

D'accord. Sans plus tarder, j'ai le plaisir de vous annoncer que Object.observe() est désormais disponible dans la version stable de Chrome 36. [WOOOO. LA COURSE SE DÉVELOPPE].

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. Elle permet à un observateur de recevoir une séquence chronologique d'enregistrements de modifications décrivant 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 apporté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 avec une logique métier complexe, les cadres avisés sont inestimables et vous devez continuer à les utiliser. Elles simplifient l'orientation des nouveaux développeurs, nécessitent moins de maintenance du code et imposent des modèles sur la façon d'effectuer 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 lui apporter une amélioration des performances, 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é qu'une analyse comparative impliquant des modifications apportées à un modèle prenait 40 ms par mise à jour et O.o() prenait entre 1 et 2 ms par mise à jour (20 à 40 fois plus vite).

Grâce à la liaison de données, sans avoir besoin d'un grand nombre de codes compliqués, vous n'avez plus besoin de rechercher des modifications, ce qui prolonge l'autonomie de la batterie !

Si vous avez déjà acheté O.o(), passez directement à la présentation de cette fonctionnalité ou lisez la suite pour en savoir plus sur les problèmes qu'elle permet de résoudre.

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 des éléments de tableau sont insérés ou séparés
  • 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 des commandes 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 maintenir celui-ci à jour. Cela génère un avantage et vous fait gagner beaucoup de temps en écrivant du code vraiment répétitif qui envoie simplement des données vers et depuis 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 où vous devez établir des relations entre plusieurs propriétés de vos modèles de données avec plusieurs éléments dans vos vues. Ce cas de figure est assez courant dans les applications monopages que nous développons aujourd'hui.

En intégrant un moyen d'observer les données de manière native dans le navigateur, nous offrons aux frameworks JavaScript (et aux petites bibliothèques d'utilitaires que vous écrivez) un moyen d'observer les modifications apportées aux données du modèle sans vous appuyer sur certaines des techniques de piratage lentes que le monde utilise aujourd'hui.

À quoi ressemble le monde aujourd'hui

Vérification de l'état sale

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, etc.), vous avez probablement l'habitude de 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 associons la valeur de chaque téléphone d'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 pour le 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 sous-jacentes du modèle sont modifiées, notre liste dans le DOM est mise à jour. Comment Angular permet-il d'atteindre cet objectif ? En coulisses, il s'agit d'une opération de vérification.

Vérification de l&#39;état non conforme

L'idée de base avec la vérification sale est que chaque fois que les données ont pu changer, la bibliothèque doit aller et vérifier si elles ont changé par un condensat ou un cycle de modification. Dans le cas d'Angular, un cycle de condensé identifie toutes les expressions enregistrées à surveiller afin de déterminer s'il y a un changement. Il connaît les valeurs précédentes d'un modèle et, si elles ont été modifiées, un événement de modification est déclenché. Pour un développeur, le principal avantage réside dans le fait que vous pouvez utiliser des données d'objets JavaScript brutes, agréables à utiliser et qui se composent assez bien. L'inconvénient est qu'elle présente un mauvais comportement algorithmique et qu'elle est potentiellement très onéreuse.

Vérification non conforme.

Le coût de cette opération est proportionnel au nombre total d'objets observés. Je vais peut-être devoir faire beaucoup de vérifications. Cela peut également nécessiter un moyen de déclencher une vérification modifiée lorsque les données ont peut-être changé. Pour cela, il existe de nombreux frameworks d'astuces intelligents. On ne sait pas si tout sera parfait un jour.

L'écosystème Web devrait avoir plus de capacité à innover et à faire é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 conteneur (Ember, backbone)

Les objets Container permettent à un framework de créer des objets contenant les données. Ils ont accès 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 possède un bon comportement algorithmique. Vous trouverez ci-dessous un exemple d'objets 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

Le coût de la découverte de ce qui a changé ici est proportionnel 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 reçues du serveur en objets pour qu'ils soient observables.

Ce code n'est pas particulièrement adapté au code JS existant, car la plupart du code suppose qu'il peut fonctionner sur des données brutes. Elle n'est pas adaptée à ces types d'objets spécialisés.

Introducing Object.observe()

Idéalement, ce que nous voulons, c'est le meilleur des deux mondes : un moyen d'observer les données avec prise en charge des objets de données brutes (objets JavaScript standards) si nous choisissons l'opérateur AND sans avoir à tout vérifier en permanence. Quelque chose avec un bon comportement algorithmique. Quelque chose qui se compose bien et qui est intégré à la plate-forme. C'est la beauté de ce que Object.observe() apporte à la table.

Elle nous permet d'observer un objet, de modifier les propriétés de mutate et de consulter le rapport de modifications des modifications. Mais assez pour la théorie, examinons du code !

Object.observe()

Object.observe() et Object.unobserve()

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

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

Vous pouvez 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 changements à l'aide de O.o(), en transmettant l'objet comme premier argument et le rappel en tant que deuxième:

Object.observe(todoModel, observer);

Commençons par apporter quelques modifications à notre objet de modèle "Tâches" :

todoModel.label = 'Buy some more milk';

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

Rapport de la console

Bravo ! Adieu le casse-tête ! Votre pierre tombale doit être gravée en Comic Sans. Modifions une autre propriété. Cette fois, completeBy:

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

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

Modifier le rapport.

Très bien. Et si nous décidions maintenant de supprimer la propriété "completed" de notre objet:

delete todoModel.completed;
Terminée

Comme vous pouvez le voir, le rapport sur les modifications renvoyées inclut des informations sur la suppression. Comme prévu, la nouvelle valeur de la propriété n'est plus définie. Nous savons donc maintenant que vous pouvez être informé lorsque des propriétés ont été ajoutées. Une fois qu'ils ont été supprimés. Fondamentalement, l'ensemble des propriétés d'un objet ("nouveau", "supprimé", "reconfiguré") et le prototype est en cours de modification (proto).

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

Object.unobserve(todoModel, observer);

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

Mutations

Spécifier des changements d'intérêt

Nous avons donc examiné les bases pour récupérer une liste des modifications apportées à un objet observé. Comment faire si vous ne vous intéressez qu'à un sous-ensemble des modifications apportées à un objet, et non à toutes ? 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'acceptations. 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é, vous pouvez constater que ce type de modification est bien signalé:

delete todoModel.label;

Si vous ne spécifiez pas de liste de types d'acceptations sur O.o(), les types de modification d'objet "intrinsiques" (add, update, delete, reconfigure, preventExtensions) sont définis par défaut (pour lorsqu'un objet qui devient non extensible n'est pas observable).

Notifications

O.o() s'accompagne également de la notion de notifications. Elles ne ressemblent en rien aux agaçantes que l'on obtient sur un téléphone, mais plutôt utiles. Les notifications sont similaires aux Observateurs de mutation. Elles ont lieu à la fin de la micro-tâche. Dans le contexte du navigateur, cette action se trouve presque toujours à la fin du gestionnaire d'événements actuel.

Le timing est agréable, car généralement une unité de travail est terminée et les observateurs peuvent maintenant faire leur travail. C'est un modèle de traitement au tour par tour.

Le workflow d'utilisation d'un système d'alerte ressemble à ceci:

Notifications

Voyons comment utiliser concrètement les systèmes d'alerte pour définir des notifications personnalisées lorsque les propriétés d'un objet sont "get" ou "set". Surveillez les commentaires ici:

// 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 lorsque la valeur des propriétés de données change ("update"). Tout autre élément choisi par l'implémentation de l'objet pour la création de rapports (notifier.notifyChange()).

Grâce à nos années d'expérience sur la plate-forme Web, nous savons que l'approche synchrone est la première chose que vous devez essayer, car c'est la plus facile à comprendre. Le problème, c'est qu'il crée un modèle de traitement extrêmement dangereux. Si vous écrivez du code et que vous décidez de mettre à jour la propriété d'un objet, vous ne voulez pas qu'une telle situation ait pu inciter un code arbitraire à faire ce qu'il voulait. Il n'est pas idéal d'invalider vos hypothèses, car vous passez par le milieu d'une fonction.

Si vous êtes un observateur, dans l'idéal, vous ne voulez pas être appelé si quelqu'un se trouve au milieu de quelque chose. Vous ne voulez pas qu'on vous demande d'aller travailler sur un état incohérent du monde. Vous finirez par faire beaucoup plus de vérification des erreurs. On essaie de tolérer beaucoup plus de situations insatisfaisantes et, en général, c'est un modèle difficile à utiliser. Le modèle asynchrone est plus difficile à gérer, mais il est en fin de compte plus efficace.

La solution à ce problème consiste à utiliser les enregistrements de modifications synthétiques.

Enregistrements de modifications synthétiques

Fondamentalement, si vous souhaitez avoir des accesseurs ou des propriétés calculées, vous êtes tenu d'envoyer une notification lorsque ces valeurs changent. Il s'agit d'une sorte de fonctionnalité de première classe de ce mécanisme. Ces notifications seront envoyé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 accesseurs et des propriétés calculées peut être résolue avec notifier.notify, une autre partie de O.o(). La plupart des systèmes d'observation veulent une certaine forme d'observation des valeurs dérivées. Il existe de nombreuses façons de le faire. O.o ne juge pas de la "bonne" manière. Les propriétés calculées doivent être des accesseurs qui notify tout changement d'état interne (privé).

Là encore, 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écurrent).

Passons à l'exemple suivant, qui est une classe de cercle. L'idée ici est que nous avons ce cercle et qu'il y a une propriété de rayon. Dans le cas présent, le rayon est un accesseur. Lorsque sa valeur change, il est informé de la modification de la valeur. Il sera transmis avec toutes les autres modifications apportées à cet objet ou à tout autre objet. Globalement, si vous implémentez un objet, vous souhaitez utiliser des propriétés synthétiques ou calculées, ou si vous devez choisir une stratégie pour y parvenir. Une fois que vous aurez fait, cela s'intégrera à votre système dans son ensemble.

Passez au-delà du code pour voir 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 d'accesseur. Comme indiqué précédemment, seules les modifications de valeur sont observables pour les propriétés des données. Non pour les propriétés ou accesseurs calculés. En effet, JavaScript n'a pas vraiment la notion de changement de valeur pour les accesseurs. Un accesseur n'est qu'un ensemble de fonctions.

Si vous attribuez le code JavaScript à un accesseur, il suffit d'appeler la fonction à cet endroit. De son point de vue, rien n'a changé. Cela permettait simplement à du code de s'exécuter.

D'un point de vue sémantique, nous pouvons examiner l'affectation ci-dessus à la valeur "5". Nous devrions être en mesure de savoir ce qui s'est passé ici. C'est en fait un problème insoluble. L'exemple montre pourquoi. Il n'y a vraiment aucun moyen pour un système de savoir ce que cela signifie, car il peut s'agir de code arbitraire. Dans ce cas, il peut faire tout ce qu'il veut. Il met à jour la valeur à chaque accès, et demander si elle a changé n'est pas très logique.

Observer plusieurs objets avec un seul rappel

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

Observer plusieurs objets avec un seul rappel

Modifications à grande échelle

Peut-être travaillez-vous sur une application vraiment volumineuse et devez régulièrement apporter des modifications à grande échelle. Les objets peuvent décrire des changements sémantiques plus importants, ce qui affectera de nombreuses propriétés de manière plus compacte (au lieu de diffuser un grand nombre de modifications de propriétés).

La méthode O.o() y est utile grâce à deux utilitaires spécifiques: notifier.performChange() et notifier.notify(), que nous avons déjà présentés.

Modifications à grande échelle

Examinons un exemple illustrant comment des changements à grande échelle peuvent être décrits en définissant un objet Thingy avec des utilitaires mathématiques (multiplier, incrémenter, incrémenterAndMultiply). 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: un qui est un collecteur des modifications et un autre qui ne génère des rapports 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-le, puis effectuez quelques modifications. Incroyable ! Beaucoup de trucs !

// 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 dans la fonction « performer » est considéré comme le travail de « grand changement ». Les observateurs qui acceptent « grand changement » ne recevront que l'enregistrement « grand changement ». Les observateurs qui ne recevront pas les modifications sous-jacentes résultant du travail effectué par la fonction « réaliser la fonction »

Observer des tableaux

Nous avons parlé depuis un moment de l'observation des modifications apportées aux objets, mais qu'en est-il des tableaux ? Excellente question. Quand quelqu'un me dit : C'est une excellente question. Je n'entends jamais leur réponse parce que je suis occupée à me féliciter d'avoir posé une question aussi intéressante, mais je suis dégoûté. Nous proposons également de nouvelles méthodes pour utiliser les tableaux.

Array.observe() est une méthode qui traite les changements à grande échelle de lui-même (par exemple, splice, non-décalage ou tout ce qui modifie implicitement sa longueur) comme un enregistrement de modification de type "splice". En interne, elle utilise notifier.performChange("splice",...).

Voici un exemple dans lequel nous observons un "tableau" de modèle et obtenons également 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

L'impact de O.o() sur les performances de calcul est de la considérer comme un cache de lecture. De manière générale, un cache constitue un excellent choix lorsque (par ordre d'importance):

  1. La fréquence des lectures prévaut sur celle des écritures.
  2. Vous pouvez créer un cache qui échange la quantité constante de travail impliquée lors des écritures afin d'obtenir de meilleures performances algorithmiques pendant les lectures.
  3. Le ralentissement dans le temps constant des écritures est acceptable.

O.o() est conçue pour des cas d'utilisation comme 1).

La vérification sale 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 structurelle pour des vérifications incorrectes que vous n'obtenez pas avec O.o(). Ces vérifications, bien qu'elles constituent une solution corrective de rechange, sont également une abstraction fondamentale qui fuit, ce qui peut créer une complexité inutile pour les applications.

Pourquoi ? Eh bien, la vérification 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 pour y parvenir, et une approche quelconque présente des inconvénients importants (par exemple, vérifier un intervalle d'interrogation peut entraîner des artefacts visuels et des conditions de concurrence entre les problèmes de code). La vérification modifiée 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 comparatifs ci-dessous (disponibles sur GitHub) nous permettent de comparer les vérifications incorrectes et O.o(). Ils sont structurés comme des graphiques comparant les valeurs "Observd-Object-Set-Size" et "Number-Of-Mutations". En général, les performances de la vérification incorrecte sont proportionnelles au nombre d'objets observés d'un point de vue algorithmique, tandis que les performances de O.o() sont proportionnelles au nombre de mutations effectuées.

Vérification de l'état sale

Vérification des performances sales

Chrome avec Object.observe() activé

Observer les performances

Polyfilling Object.observe()

Parfait. O.o() peut donc être utilisée 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 qui le polyfille et inclut des sucres utiles par-dessus. Elle offre une vue globale du monde qui résume les changements et fournit un rapport sur ce qui a changé. Il expose deux éléments très puissants:

  1. Vous pouvez observer les chemins. Cela signifie que si vous souhaitez observer "foo.bar.baz" à partir d'un objet donné, vous serez informé lorsque la valeur de ce chemin d'accès a changé. Si le chemin est inaccessible, il considère que la valeur n'est pas définie.

Exemple d'observation d'une valeur au niveau d'un chemin à 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 indique les splices de tableau. Les plis de tableau correspondent à l'ensemble minimal d'opérations d'insertion que vous devrez effectuer sur un tableau pour transformer l'ancienne version du tableau en la nouvelle version. Il s'agit d'un type de transformation ou d'une autre vue du tableau. Il s'agit de la quantité minimale de travail que vous devez effectuer pour passer de l'ancien état au nouvel état.

Exemple de rapports sur les modifications apportées à un tableau sous la forme d'un ensemble minimal d'insertions:

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() donnera aux frameworks et aux bibliothèques une occasion unique 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 prise en charge d'O.o() figure dans la feuille de route à court terme d'Ember. Misko Hervy, d'Angular, a rédigé un document de conception sur l'amélioration de la détection des modifications dans Angular 2.0. L'approche à long terme consistera à exploiter Object.observe() lorsqu'il sera disponible dans la version stable de Chrome, en optant pour Watchtower.js, sa propre méthode de détection des modifications jusqu'à cette date. C'est génial.

Conclusions

O.o() est un puissant ajout à la plateforme Web que vous pouvez utiliser aujourd'hui.

Nous espérons qu'à terme, cette fonctionnalité sera disponible dans d'autres 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 personnes ciblant 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.

Discutez de Object.observe() avec les auteurs de frameworks JavaScript et expliquez-leur comment ils envisagent de l'utiliser pour améliorer les performances de la liaison de données dans vos applications. C'est une période passionnante qui vous attend !

Ressources

Nous remercions Rafael Weinstein, Jake Archibald, Eric Bidelman, Paul Kinlan et Vivian Cromwell pour leurs commentaires et leurs avis.