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

Introduction

Une révolution est en marche. Une nouvelle fonctionnalité a été ajoutée à JavaScript. Elle va tout changer dans ce que vous pensiez savoir sur la liaison de données. Cela va également modifier la façon dont de nombreuses bibliothèques MVC observent les modèles pour les modifications et les mises à jour. Êtes-vous prêt à améliorer les performances des applications qui s'intéressent à 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 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 signifie pas que vous ne devez pas en utiliser. Pour les grands projets avec 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 de l'état de modification prenait 40 ms par mise à jour et O.o() 1 à 2 ms par mise à jour (une amélioration de 20 à 40 fois plus rapide).

La liaison de données sans avoir besoin de tonnes de code complexe signifie également que vous n'avez plus besoin d'interroger les modifications, ce qui prolonge l'autonomie de la batterie.

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 des éléments sont insérés ou supprimés de tableaux
  • 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 de modèle et de vue. Le code HTML est un excellent mécanisme déclaratif, mais il est complètement statique. Idéalement, vous souhaitez simplement déclarer la relation entre vos données et le DOM, et mettre à jour le DOM. Cela vous permet de gagner du temps en écrivant du code très répétitif qui n'envoie que 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 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 entier.

À quoi ressemble le monde aujourd'hui ?

Vérification de l'état de modification

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 de l'état de modification.

Vérification sale

L'idée de base de la vérification de l'état des données est que chaque fois que des données peuvent avoir changé, la bibliothèque doit vérifier si elles ont changé via un récapitulatif ou un cycle de modification. 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.

Vérification sale

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é. Vous devrez peut-être également trouver un moyen de déclencher la vérification des données lorsque celles-ci peuvent avoir changé. Les frameworks utilisent de nombreuses astuces pour ce faire. Il n'est pas certain que 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, la persistance des modifications apportées à IndexedDB ou localStorage)
  • Objets de conteneur (Ember, Backbone)

Les objets conteneur sont des objets créés par un framework qui 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

Le coût de la découverte des modifications est proportionnel au nombre de modifications apportées. 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 que vous recevez du serveur en 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. Ce n'est pas le cas pour ces types d'objets spécialisés.

Introducing Object.observe()

Idéalement, nous voulons le meilleur des deux mondes : un moyen d'observer les données avec la prise en charge des objets de données brutes (objets JavaScript standards) si nous le souhaitons, ET sans avoir à effectuer des vérifications de modification en permanence. Un élément avec un bon comportement algorithmique. Un élément qui se compose bien et qui est intégré à la plate-forme. C'est toute la beauté de Object.observe().

Il nous permet d'observer un objet, de modifier des propriétés et d'afficher le rapport de modification de 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 devrait être gravée en Comic Sans. Passons à une autre propriété. Cette fois, completeBy:

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

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

Modifier le rapport

Parfait. Supposons que nous décidions maintenant 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. Lorsque les données ont été supprimées. En gros, le set de propriétés sur un objet ("new", "deleted", "reconfigured") et son prototype changent (proto).

Comme dans tout système d'observation, il existe également une méthode pour arrêter d'écouter les modifications. 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, aucune mutation apportée à l'objet après cette exécution ne renvoie plus de liste d'enregistrements de modification.

Mutations

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

Nous avons donc vu les principes de base pour obtenir une liste des modifications apportées à un objet observé. Que faire si vous ne vous intéressez qu'à un sous-ensemble des modifications apportées à un objet plutôt qu'à toutes ? Tout le monde a besoin d'un filtre antispam. Les observateurs ne peuvent spécifier que les types de modifications qu'ils souhaitent connaître 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:

// 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 à O.o(), les types de modification d'objet "intrinsèques" (add, update, delete, reconfigure, preventExtensions (lorsque la non-extensibilité d'un objet n'est pas observable)) sont utilisés par défaut.

Notifications

O.o() inclut également 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. Ils se produisent à la fin de la micro-tâche. Dans le contexte du navigateur, cela se produit 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. Il s'agit d'un bon modèle de traitement par tour.

Le workflow d'utilisation d'un notifier se présente comme suit:

Notifications

Prenons un exemple d'utilisation pratique des notifiers pour définir des notifications personnalisées lorsque les propriétés d'un objet sont récupérées 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 Notifications

Ici, nous signalons lorsque la valeur des propriétés de données change ("update"). 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 observateur, vous ne voulez idéalement pas être appelé si quelqu'un est en train de faire 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 difficiles et, en général, c'est un modèle difficile à utiliser. 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

En gros, si vous souhaitez utiliser des accesseurs ou des propriétés calculées, il vous incombe de les 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 procéder. O.o ne fait aucun jugement sur la "bonne" méthode. Les propriétés calculées doivent être des accesseurs qui notifient lorsque l'état interne (privé) change.

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 et, lorsque sa valeur change, il s'en notifiera lui-même. 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 modification synthétiques

Propriétés de l'accesseur

Remarque rapide sur les propriétés d'accesseur. Nous avons mentionné précédemment que seules les modifications de valeur sont observables pour les propriétés de données. Ne s'applique pas aux propriétés calculées ni aux accesseurs. En effet, JavaScript n'a pas vraiment la notion de modification de valeur des accésseurs. Un accesseur n'est qu'une collection 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é. Il a simplement donné la possibilité à du code d'être exécuté.

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é. Il s'agit en fait d'un problème insoluble. L'exemple ci-dessous 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 recevra l'ensemble complet des modifications apportées à tous les objets qu'il observe à la fin de la micro-tâ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 grande et devez régulièrement gérer des modifications à grande échelle. Les objets peuvent souhaiter décrire des modifications sémantiques plus importantes qui affecteront de nombreuses propriétés de manière plus compacte (au lieu de diffuser des tonnes de modifications de propriétés).

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 qui regroupe toutes les modifications et l'autre qui ne signale que 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 un nouvel élément Thingy:

var thingy = new Thingy(2, 4);

Observez-la, puis apportez des modifications. OMG, c'est tellement amusant. Tellement de choses !

// 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 d'exécution" est considéré comme le travail de "big-change". Les observateurs qui acceptent "big-change" ne recevront que l'enregistrement "big-change". Les observateurs qui ne le font pas recevront les modifications sous-jacentes résultant du travail effectué par "exécuter la fonction".

Observer des tableaux

Nous avons déjà parlé 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, car je suis occupé à me féliciter d'avoir posé une question aussi pertinente. Mais je m'égare. Nous avons également de nouvelles méthodes pour travailler avec des tableaux.

Array.observe() est une méthode qui traite les modifications à grande échelle d'elle-même (par exemple, l'épissage, le décalage ou tout élément qui modifie implicitement sa longueur) en tant qu'enregistrement de modification "épissage". 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é de travail constante impliquée lors des écritures contre 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 comme 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 robuste pour ce faire, et toute approche présente des inconvénients importants (par exemple, la vérification d'un intervalle de sondage risque de générer des artefacts visuels et des conditions de course entre les problèmes de code). La vérification de l'état de modification nécessite également un registre global des observateurs, ce qui crée des risques de fuite de mémoire et des coûts de démontage qu'O.o() évite.

Examinons quelques chiffres.

Les tests de référence ci-dessous (disponibles sur GitHub) nous permettent de comparer la vérification de l'état des données à O.o(). Ils sont structurés sous forme de graphiques de la taille de l'ensemble d'objets observés par rapport au nombre de mutations. Le résultat général est que les performances de la vérification de l'état de modification sont proportionnelles algorithmiquement au nombre d'objets observés, tandis que les performances de O.o() sont proportionnelles au nombre de mutations effectuées.

Vérification de l'état de modification

Performances de la vérification de l&#39;état de l&#39;objet

Chrome avec Object.observe() activé

Observer les performances

Polyfilling Object.observe()

Parfait. O.o() peut donc être utilisé dans Chrome 36, mais qu'en est-il de son utilisation 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. Il offre une vue globale du monde qui résume les changements et fournit un rapport sur ce qui a changé. Il présente deux fonctionnalités très puissantes:

  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 d'accès est inaccessible, la valeur est considérée comme non définie.

Exemple d'observation d'une valeur à 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 en dira plus sur les épissures 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 indiqué, O.o() offre aux frameworks et aux bibliothèques une opportunité 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 compatibilité avec O.o() figure dans la feuille de route à court terme d'Ember. Misko Hervy, de l'équipe Angular, a rédigé un document de conception sur la détection des modifications améliorée d'Angular 2.0. Leur approche à long terme consistera à exploiter Object.observe() lorsqu'il sera disponible dans Chrome stable, en optant pour Watchtower.js, leur propre approche 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 que cette fonctionnalité sera disponible dans davantage de navigateurs à terme, ce qui permettra aux frameworks JavaScript d'améliorer leurs performances grâce à l'accès aux fonctionnalités d'observation d'objets natives. 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.

Alors, n'hésitez pas à discuter avec les auteurs de 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. De belles choses nous attendent !

Ressources

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