다음을 통해 공유


Reactive Extensions for JavaScript: The Time flies like an arrow sample

As we just released the Reactive Extensions for Javascript I thought it would be nice to dig a bit deeper in what the sample that ships with the download is all about.

For those who haven’t downloaded the bits yet, or haven’t gotten the time to play with the sample, I’ve embedded it right here.

Try moving the mouse over this box

So let's dig into the details of this sample.

  • First we need to include jQuery and the Rx for JS library:

    <script src="https://code.jquery.com/jquery-latest.js" type="text/javascript"></script><script src="rx.js" type="text/javascript"></script>

  • Next we need to get notifications everytime a mousemove event comes in from the document via jQuery. To do this, we ask the Rx library to give us an observable collection that represents a jQuery event. the Rx library will call OnNext on observers subscribed to this observable every mouse move.

    var mouseMove = Rx.Observable.FromJQueryEvent($(document), "mousemove");

  • As we want to move each character over the screen seperately, we need to create ui elements for each character and add them to the DOM:

    var text = "time flies like an arrow";var container = $("#container");for (var i = 0; i < text.length; i++){     var s = $(document.createElement("span")); s.html(text.charAt(i)); s.css({ position: "absolute"}); s.appendTo(container);}

  • Now let's try to hook up the mouse move observable to the individual elements. In Rx this is done by subscribing an Observer to an Observable. An Observer has an OnNext, OnError and OnCompleted message. As often we're only interested in OnNext, we can simply ignore the OnError and OnCompleted functions and Rx will automatically pick defaults for those methods. (OnError will throw and OnCompleted becomes a no-op):

    var text = "time flies like an arrow";var container = $("#container");for (var i = 0; i < text.length; i++){     var s = $(document.createElement("span")); s.html(text.charAt(i)); s.css({ position: "absolute"}); s.appendTo(container);

       mouseMove.Subscribe(function(mouseEvent) { s.css({ top: mouseEvent.offsetY + "px", left: mouseEvent.offsetX + "px" });    });}

  • As you can see only the last character starts moving. The reason for this is the way that javascript closures work. As the subscribe function is executed only when mousemove fires, it is not executed as part of the scope of the for loop. As the value of s gets overridden for each itteration of the for loop, s will point to the last character of the string by the time the mousemove event is triggered.

    We can change this behavior by wrapping the body of the for loop in a function, which will change the scoping of the variables:

    var text = "time flies like an arrow";var container = $("#container");for (var i = 0; i < text.length; i++){     function(i) { var s = $(document.createElement("span")); s.html(text.charAt(i)); s.css({ position: "absolute"}); s.appendTo(container);

           mouseMove.Subscribe(function(mouseEvent) { s.css({ top: mouseEvent.offsetY + "px", left: mouseEvent.offsetX + "px" });        }); })(i);}

  • Let's make a small layout modification to make the letters layout next to each other instead of on top of each other by shifting the left position by a multiple of the position of the character:

    left: mouseEvent.clientX + i * 10 + 15 +

    "px"
  • As all the different characters get the mousemove notification at the same time, the text follows the mouse as a single line. Let's look at how Rx can help us make each letter lag just a bit more behind the mouse cursor. To do this we're going to modify our observable collection. Rx has a whole algebra of operations you can perform on observable collections. The operator we're going to look at is Delay.

    Delay is defined on the Rx.Observable prototype and it takes a dueTime as an argument, it returns a new observable collection. When a notification arrives on the original observable collection, the operator will delay that notification for the time specified by dueTime and after that time will fire out the notification on the resulting observable collection.

    Rx is a fluent api so we can insert the call to Delay directly in one of our expressions dealing with an observable collection:

    mouseMove.Delay(i * 100).Subscribe(

    function(mouseEvent)

    Leading to the final version:

    var text = "time flies like an arrow";var container = $("#container");for (var i = 0; i < text.length; i++){     function(i) { var s = $(document.createElement("span")); s.html(text.charAt(i)); s.css({ position: "absolute"}); s.appendTo(container);

           mouseMove.Delay(i * 100).Subscribe(function(mouseEvent) { s.css({ top: mouseEvent.offsetY + "px", left: mouseEvent.offsetX mouseEvent.clientX + i * 10 + 15 + "px" });        }); })(i);}

That's it, we just finished the Hello World of Rx. We're using conversions from existing events into observable collections, subscribing to the observable and finally using one of the time based operators to modify the observable collection.

You can download Rx for JavaScript on Rx's devlab page. Questions can be asked on the Rx forum. If you're looking for more samples about Rx for JavaScript, I can recommend Matthew Podwysocki's blog.

Also don't forget to follow @jvgogh and #RxJS on twitter

Comments

  • Anonymous
    March 17, 2010
    Thanks for the good article and the nice work on Rx.

  • Anonymous
    March 21, 2010
    There is just a little typo: the opening round bracket is missing in front of the function definition. It should be for (var i = 0; i < text.length; i++) {    (function(i)    {...}); }

  • Anonymous
    April 01, 2010
    The jQuery sample code you've provided is the poster child for "bad practice" var s = $(document.createElement("span"));? s.html(text.charAt(i)); s.css({ position: "absolute"}); s.appendTo(container); try: $('<span/>', {  html: text.charAt(i),  css: { position: "absolute"} }).appendTo(container); Merely one example. I'm not going to bother re-writing all of your poorly demonstrated jQuery.

  • Anonymous
    April 01, 2010
    Sorry, I was so annoyed I forgot this: $('<span/>', { html: text.charAt(i), css: { position: "absolute"} }).appendTo('#container'); // <-- no need to be WASTEFUL with global vars like: var container = $('#container')

  • Anonymous
    April 01, 2010
    Dear Rick, If you provide a sample that looks nicer and performs as well as mine, I'd be more than happy to update this blog post with your code. Jeffrey

  • Anonymous
    April 01, 2010
    No problem. http://jsbin.com/ojafu There you go. It also works smoother then yours in FF3.6, IE8, Safari & Chrome (both latest) I used up-to-date jQuery 1.4.2 practices, proper var declaration, etc. Also, your version omitted the mouseMove yadda-yadda stuff - best not to let people think that was magic right? I also kept your Rx.js stuff intact to maintain the usefulness as an Rx demo. Enjoy.

  • Anonymous
    April 01, 2010
    I added a few comments explaining some jQuery performance principles: http://jsbin.com/ojafu/3

  • Anonymous
    April 02, 2010
    While you're refactoring the code, Rick, you might want to make things more flexible, as in this example: http://jsbin.com/ojafu/6

  • Ben
  • Anonymous
    April 02, 2010
    Mr. Alman, you never cease to amaze.

  • Anonymous
    April 02, 2010
    Hi Rick & Ben, Awesome changes to the sample! Will update this blog post over the weekend with your suggestions. Thanks! Jeffrey

  • Anonymous
    July 30, 2012
    Bummer, it's broken since it tries to load Rx from jeffrey.reactiveextensions.net/rx_old.js which no longer exists... and this page is the second result when googling "rx javascript examples"

  • Anonymous
    September 13, 2012
    Unfortunately the inline samples no longer work because they were using an internal link to the rx.js libraries that are no longer available.

  • Anonymous
    December 05, 2012
    Um... none of these blue boxes do anything when I move my mouse over them.  I tried in both IE and Chrome. What's supposed to happen?