Search code examples
javascriptjqueryrxjsflatmapconcatmap

Strange behavior of Rx.Observable.prototype.fromEvent()


Today I've seen a strange problem when using RxJS. Please help me inspect it.

The problem I am working on is: "Given an array of image URLs, load and append all images to a div."

All the code snippets to demonstrate is here:

https://pastebin.com/F3ZkH3F8

At the beginning, I used the first snippet.

However, Rx.Observable.prototype.flatMap sometimes puts the images in wrong order (this behavior is noticed in the documentation). So, I changed to use concatMap (second snippet).

This time, only the first image is loaded. I took some time inspect the problem. I doubt that event load is not trigged from image. But the most confusing situation is that when I add some code to listen to image's load event only, it showed me that the event is trigged properly... (third snippet).

Then I decided to write another version using $.Deferred (fourth snippet).

It worked...

Could you please tell me what is the problem? Thank you very much!


Solution

  • Because fromEvent(image, 'load') on the first sub-observable is not completed, other sub-observables are waiting forever. So you should complete sub-observable after first event.

    Use take(1).

    excerpt from your second snippet

    ...
    var loadedImageStream = Rx.Observable
                                .fromEvent(image, 'load')
                                .map(function() {                                  
                                    return image;
                                 })
    ...
    

    Add take(1) to complete sub-observable

    ...
    var loadedImageStream = Rx.Observable
                                .fromEvent(image, 'load')
                                .map(function() {                                  
                                    return image;
                                 })
                                .take(1)
    ...
    

    EDIT:

    Using concatMap makes loading image sequential, so it is slow.

    If you pass index, you can use replace instead of append to keep the order. In this case, you can use flatMap, which enables fast concurrent loading.

    $(function () {
        var imageURLList = [
            'https://placehold.it/500x100',
            'https://placehold.it/500x200',
            'https://placehold.it/500x300',
            'https://placehold.it/500x400',
            'https://placehold.it/500x500'
        ];
        var imagesDOM = $('#images');
    
        Rx.Observable
            .fromArray(imageURLList)
            .do(function (imageURL) {
                imagesDOM.append(new Image()); // placeholder
            })
            .flatMap(function (imageURL, index) {
                var image = new Image();
    
                var loadedImageStream = Rx.Observable
                    .fromEvent(image, 'load')
                    .map(function () {
                        return [image, index];
                    })
                    .take(1)
    
                image.src = imageURL;
    
                return loadedImageStream;
            })
            .subscribe(function (image_index) {
                var image = image_index[0];
                var index = image_index[1];
    
                imagesDOM.children().get(index).replaceWith(image);
            })
    })