Reactive Extensions for Javascript (RxJS Framework)

Publicado em 21/11/2011

1


Olá pessoal. Tudo certo!?

No post de hoje, apresento alguns conceitos fundamentais para utilização de Rx com Javascript.

Rx Framework foi desenvolvido, inicialmente, para .NET e Silverlight. Mais tarde, foi portado para Javascript. Com ele, podemos simplificar bastante a implementação de rotinas assíncronas, além de poder combinar (compose) eventos.

Se desejar ver alguns exemplos desse framework com .NET, dê uma olhada nos outros posts que escrevi sobre o tema.

Onde obter RxJS?

Atualmente, RxJS pode ser obtido em http://msdn.microsoft.com/en-us/data/gg577610. Nesse download, estão disponíveis, além da biblioteca propriamente dita, um pacote de exemplos e extensões para os principais frameworks para JS. Entre eles: JQuery, mootools e extJS.

Por que utilizar RxJS?

Tentando manter a simplicidade:

  • Há uma necessidade crescente de suportar callbacks em nossos códigos (programação assíncrona);
  • Há modelos e cenários diversificados para implementação de programação assíncrona (DOM Events, JQuery, AJAX);
  • O código pode ficar potencialmente mais limpo através de técnicas como unification e composition.

Recapitulando RxJS

Sempre que estivermos falando em RxJS, teremos dois componentes a considerar:

  • Um (ou mais) objeto(s) observable, que emite notificações (ex: lança eventos);
  • Um (ou mais) objeto(s) observer, que monitora(m) as notificações emitidas (push) pelo observable.

O objeto observer informa o objeto observable que deseja ser notificado, através de uma “incrição” (subscribe).

As notificações enviadas pelo observable são recebidas no objeto observer a partir de três funções simples:

  1. onNext – que será acionada sempre que o observable emitir uma notificação;
  2. onError – que será acionada se houver alguma exception – nesse caso, não vão ocorrer notificações futuras;
  3. onCompleted  – que será acionada quando o observer entender que não há mais notificações a disparar.

Veja esse exemplo simples (html completo, em https://gist.github.com/1383995)


              
$(function () {
    var observable = Rx.Observable.Range(0, 10);
            
    var observer = observable.Subscribe(
        function (e) { document.write("onNext: " + e + "
"); }, /* onNext */ function (e) { document.write("onError: " + e + "
"); }, /* onError */ function () { document.write("OnCompleted") } /* onCompleted */ ); observer.Dispose(); });

            

Que resulta em:

image

Como pode perceber:

  1. começo criando um objeto observable através de um recurso do próprio framework;
  2. faço a “incrição” (subscribe) gerando um objeto observer;
  3. descarto o objeto observer (o que cancela a assinatura).

Durante a execução, a função onNext é acionada uma vez para cada um dos elementos do intervalo. No fim, a função onCompleted é chamada.

Importante destacar que as funções onError e onCompleted são opcionais.

Built-in Observable Sequences Factory Functions

No código que demonstrei acima, utilizei uma Observable Sequence Factory Functions. Há algumas outras. Perceba:

Range

Gera uma chamada para a função onNext para cada elemento da enumeração.


              
var source = Rx.Observable.Range(0, 10);
source.Subscribe(function(x) { /* 0..9 */ });

            

Return

Gera uma única chamada para a função onNext.


              
var source = Rx.Observable.Return("RxJS");
source.Subscribe(function(s) { /* recebe“RxJS” */ });

            

FromArray

Gera uma chamada para cada elemento do array passado como parâmetro para a factory function.


              
var source = Rx.Observable.FromArray(["a", "b", "c"]);
source.Subscribe(function(s) { /* ‘a’..‘c’ */ });

            

Timer

Gera uma chamada sempre que um intervalo de tempo for acionado.


              
var observable = Rx.Observable.Timer(1000, 2000);
var observer = observable.Subscribe(
    function (e) { $("#target").text(e); }/* onNext */
);

            

Repare que, nesse exemplo, a primeira chamada acontece após 1 segundo. As demais, em intervalos de dois segundos.

image

Usando Observables como consultas

Por que usar Rx para “assinar” eventos, afinal?! Porque podemos usar uma abordagem semelhante a consultas LINQ. Observe:


              
var o = Rx.Observable.Range(0, 50)
    .Where(function (x) { return x % 2 === 0 });

            

Primeiros 4:


              
var o = Rx.Observable.Range(0, 50)
    .Where(function (x) { return x % 2 === 0 })
    .Take(4);

            

Últimos 4:


              
var o = Rx.Observable.Range(0, 50)
    .Where(function (x) { return x % 2 === 0 })
    .TakeLast(4);

            

Ignorando os primeiros 5 elementos:


              
var o = Rx.Observable.Range(0, 50)
    .Where(function (x) { return x % 2 === 0 })
    .Skip(5);

            

E por aí, vai Smiley de boca aberta

Convertendo eventos em coleções Observables

RxJS possui suporte extendido a eventos. Aliás, essa característca levou o Rx original a ser chamado LINQ to Events.

Podemos “assinar” eventos usando DOM puro. Observe (html completo em https://gist.github.com/1384156).


              
Rx.Observable.FromHtmlEvent(window, "load").Subscribe(function () {
    var canvas = document.getElementById("myCanvas");
    var mousemove = Rx.Observable.FromDOMEvent(canvas, "mousemove");

    mousemove.Subscribe(function (e) {
        document.getElementById("target").innerHTML = e.clientX + ", " + e.clientY;
    });
});

            

Ou podemos usar JQuery (html completo em https://gist.github.com/1384173).


              
$(function () {
    var mousemove = $("#myCanvas").toObservable("mousemove");
    mousemove.Subscribe(function (e) {
        $("#target").text(e.clientX + ", " + e.clientY);
    });
});

            

Composição de eventos

A partir do momento que conseguimos “escutar” eventos como consultas, podemos trabalhar composição. Observe (html completo em https://gist.github.com/1384388):


              
$(function () {
    var mouseMove = $(document).toObservable("mousemove");
    var mouseDown = $(document).toObservable("mousedown");
    var mouseUp = $(document).toObservable("mouseup");

    var canvas = $("#target");
    var context = canvas.get(0).getContext('2d');

    function getCoords(e) {
        if (e.offsetX)
            return { x: e.offsetX, y: e.offsetY };
        return { x: e.pageX - canvas.get(0).offsetLeft, y: e.pageY - canvas.get(0).offsetTop };
    }

    var mouseMoveWithButtonDown = mouseDown.SelectMany(function (down) {
        return mouseMove
            .Select(function (move) {
                return {
                    start: getCoords(down),
                    current: getCoords(move)
                }
            })
            .TakeUntil(mouseUp)
    }
    );

    mouseMoveWithButtonDown.Subscribe(function (e) {
        context.beginPath();
        context.moveTo(e.start.x, e.start.y);
        context.lineTo(e.current.x, e.current.y);
        context.stroke();
    });
});

            

Repare a composição mouseMoveWithButtonDown. Na prática, estou monitorando os movimentos do mouse, a partir de um “mousedown”, até que ocorra um “mouseup”. Além disso, estou “montando” um registro mais interessante para nosso contexto, com a posição onde o mouse foi pressionado e a posição atual do ponteiro.

image

O live demo está aqui: http://users.cjb.net/livedemoelemarjr/rx_mouse.htm

Composição de eventos – algo mais avançado

Já conseguimos compor eventos. Agora, para mostrar o potencial do framework, vamos fazer um registro de uma ocorrência anterior e usar como “input” para uma ocorrência atual. Observe (https://gist.github.com/1384596):


              
$(function () {
    var mouseMove = $(document).toObservable("mousemove");
    var mouseDown = $(document).toObservable("mousedown");
    var mouseUp = $(document).toObservable("mouseup");

    var canvas = $("#target");
    var context = canvas.get(0).getContext('2d');

    function getCoords(e) {
        if (e.offsetX)
            return { x: e.offsetX, y: e.offsetY };
        return { x: e.pageX - canvas.get(0).offsetLeft, y: e.pageY - canvas.get(0).offsetTop };
    }

    var mouseMoves = mouseMove
        .Skip(1)
        .Zip(mouseMove, function (left, right) {
            return {
                before: getCoords(left),
                current: getCoords(right)
            };
        });

    var mouseMoveWithButtonDown = mouseDown.SelectMany(function (down) {
        return mouseMoves
            .Select(function (r) {
                return {
                    start: getCoords(down),
                    before: r.before,
                    current: r.current
                }
            })
            .TakeUntil(mouseUp)
    }
    );

    mouseMoveWithButtonDown.Subscribe(function (e) {
        context.beginPath();
        context.strokeStyle = "#f00";
        context.moveTo(e.start.x, e.start.y);
        context.lineTo(e.current.x, e.current.y);
        context.stroke();

        context.beginPath();
        context.strokeStyle = "#00f";
        context.moveTo(e.before.x, e.before.y);
        context.lineTo(e.current.x, e.current.y);
        context.stroke();
                
    });
});

            

O que fizemos?!

  • Criamos uma composição para “zipar” duas ocorrências de mouseMove;
  • Combinamos as ocorrências em um registro com o “ponto anterior” e com  o “ponto atual”;
  • Desenho um contorno.

image

 

Live demo: http://users.cjb.net/livedemoelemarjr/rx_mouse2.htm

Há muito mais possibilidades. Mas, por hoje é isso!

Smiley piscando

Etiquetado:, ,
Publicado em: Post