web-dev-qa-db-fra.com

Node.js Streams vs Observables

Après avoir appris Observables , je les trouve assez similaires à Node.js streams . Les deux ont un mécanisme pour informer le consommateur chaque fois que de nouvelles données arrivent, une erreur se produit ou il n'y a plus de données (EOF).

J'aimerais en savoir plus sur les différences conceptuelles/fonctionnelles entre les deux. Merci!

69
urish

Les observables et les flux de node.js vous permettent de résoudre le même sous-jacent problème: traiter de manière asynchrone une séquence de valeurs. La principale différence entre les deux, je crois, est liée au contexte qui a motivé son apparition. Ce contexte se reflète dans la terminologie et l'API.

Du côté Observables vous avez une extension à EcmaScript qui introduit le modèle de programmation réactive. Il essaie de combler l'écart entre la génération de valeur et l'asynchronicité avec les concepts minimalistes et composables de Observer et Observable.

Côté node.js et Streams , vous vouliez créer une interface pour le traitement asynchrone et performant des flux réseau et des fichiers locaux. La terminologie dérive de ce contexte initial et vous obtenez pipe, chunk, encoding, flush, Duplex, Buffer, etc. En ayant une approche pragmatique qui fournit un support explicite pour des cas d'utilisation particuliers, vous perdez une certaine capacité à composer des choses parce que ce n'est pas aussi uniforme. Par exemple, vous utilisez Push sur un flux Readable et write sur un Writable même si, conceptuellement, vous faites la même chose: publier une valeur.

Donc, dans la pratique, si vous regardez les concepts et si vous utilisez l'option { objectMode: true }, Vous pouvez faire correspondre Observable avec le flux Readable et Observer avec le flux Writable. Vous pouvez même créer des adaptateurs simples entre les deux modèles.

var Readable = require('stream').Readable;
var Writable = require('stream').Writable;
var util = require('util');

var Observable = function(subscriber) {
    this.subscribe = subscriber;
}

var Subscription = function(unsubscribe) {
    this.unsubscribe = unsubscribe;
}

Observable.fromReadable = function(readable) {
    return new Observable(function(observer) {
        function nop() {};

        var nextFn = observer.next ? observer.next.bind(observer) : nop;
        var returnFn = observer.return ? observer.return.bind(observer) : nop;
        var throwFn = observer.throw ? observer.throw.bind(observer) : nop;

        readable.on('data', nextFn);
        readable.on('end', returnFn);
        readable.on('error', throwFn);

        return new Subscription(function() {
            readable.removeListener('data', nextFn);
            readable.removeListener('end', returnFn);
            readable.removeListener('error', throwFn);
        });
    });
}

var Observer = function(handlers) {
    function nop() {};

    this.next = handlers.next || nop;
    this.return = handlers.return || nop;
    this.throw = handlers.throw || nop;
}

Observer.fromWritable = function(writable, shouldEnd, throwFn) {
    return new Observer({
        next: writable.write.bind(writable), 
        return: shouldEnd ? writable.end.bind(writable) : function() {}, 
        throw: throwFn
    });
}

Vous avez peut-être remarqué que j'ai changé quelques noms et utilisé les concepts plus simples de Observer et Subscription, présentés ici, pour éviter la surcharge de responsabilités effectuée par Observables dans Generator. Fondamentalement, le Subscription vous permet de vous désinscrire du Observable. Quoi qu'il en soit, avec le code ci-dessus, vous pouvez avoir un pipe.

Observable.fromReadable(process.stdin).subscribe(Observer.fromWritable(process.stdout));

Par rapport à process.stdin.pipe(process.stdout), vous disposez d'un moyen de combiner, filtrer et transformer des flux qui fonctionne également pour toute autre séquence de données. Vous pouvez y parvenir avec les flux Readable, Transform et Writable mais l'API favorise le sous-classement au lieu de chaîner Readables et d'appliquer des fonctions. Sur le modèle Observable, par exemple, la transformation de valeurs correspond à l'application d'une fonction de transformation au flux. Il ne nécessite pas de nouveau sous-type de Transform.

Observable.just = function(/*... arguments*/) {
    var values = arguments;
    return new Observable(function(observer) {
        [].forEach.call(values, function(value) {
            observer.next(value);
        });
        observer.return();
        return new Subscription(function() {});
    });
};

Observable.prototype.transform = function(transformer) {
    var source = this;
    return new Observable(function(observer) {
        return source.subscribe({
            next: function(v) {
                observer.next(transformer(v));
            },
            return: observer.return.bind(observer),
            throw: observer.throw.bind(observer)
        });
    });
};

Observable.just(1, 2, 3, 4, 5).transform(JSON.stringify)
  .subscribe(Observer.fromWritable(process.stdout))

La conclusion? Il est facile d'introduire le modèle réactif et le concept Observable n'importe où. Il est plus difficile d'implémenter une bibliothèque entière autour de ce concept. Toutes ces petites fonctions doivent fonctionner ensemble de manière cohérente. Après tout, le projet ReactiveX est toujours en cours. Mais si vous avez vraiment besoin d'envoyer le contenu du fichier au client, de gérer l'encodage et de le compresser, le support est là, dans NodeJS, et cela fonctionne plutôt bien.

87
m4ktub