如何使用RxJs异步加载图片并在所有图片加载完成后执行方法

9
我正在尝试将基于promise的代码转换为RxJs,但很难理解Rx特别是RxJs。
我有一个路径数组。
var paths = ["imagePath1","imagePath2"];

我喜欢在Javascript中加载图片。

var img = new Image();
img.src = imagePath;
image.onload // <- when this callback fires I'll add them to the images array

当所有图片加载完成后,我想执行一个方法。 我知道这里有
Rx.Observable.fromArray(imagepathes)

还有一些类似于

Rx.Observable.fromCallback(...)

有一些类似于flatMapLatest(...)Rx.Observable.interval或基于时间的调度程序。

根据我的研究,我认为这些将是解决问题的要素,但我无法使它们组合起来运行。

那么,如何从路径数组中加载图像,并在所有图像加载完成后执行基于间隔的方法呢?

感谢任何帮助。


我使用了forkJoiin来满足类似的需求(如果我正确理解了您的需求)。 - PhiLho
7个回答

13

首先,您需要一个函数来创建一个Observable或Promise来处理单独的图像:

function loadImage(imagePath){
   return Rx.Observable.create(function(observer){
     var img = new Image();
     img.src = imagePath;
     img.onload = function(){
       observer.onNext(img);
       observer.onCompleted();
     }
     img.onError = function(err){
       observer.onError(err);
     }
   });
}

然后您可以使用它来加载所有图像

Rx.Observable
  .fromArray(imagepathes)
  .concatMap(loadImage) // or flatMap to get images in load order
  .toArray()
  .subscribe(function(images){
    // do something with loaded images
  })

3

我认为你不需要自己创建一个Observable


import { from, fromEvent } from 'rxjs';
import { mergeMap, map, scan, filter } from 'rxjs/operators';

const paths = ["imagePath1","imagePath2"];

from(paths).pipe(
   mergeMap((path) => {
      const img = new Image();

      img.src = path;
      return fromEvent(img, 'load').pipe(
          map((e) => e.target)
      );
   }),
   scan((acc, curr) => [...acc, curr], []),
   filter((images) => images.length === paths.length)
).subscribe((images) => {
   // do what you want with images
});

错别字应该是 filter((images) => images.length === paths.length) - Kevin K.

1

我不认为你可以轻易地使用可观察对象来完成这个任务,因为没有任何指示结束的东西(除非你有初始大小)。 查看其他答案以获取 Rx 版本。

但是,您可以使用 Promise 数组:

/**
 * Loads an image and returns a promise
 * @param {string} url - URL of image to load
 * @return {Promise<Image>} - Promise for an image once finished loading.
 */
function loadImageAsync(url) {
    return new Promise(function(resolve, reject) {
        var img = new Image();
        img.src = imagePath;
        image.onload = function() { resolve(img); };
        image.onerror = reject;
    });
}

有了这个,您可以轻松地做到这样:

var imageUrls = ['url1', 'url2', 'url3'];
Promise.all(imageUrls.map(loadImageAsync))
    .then(function(arrayOfImageElements) {
        // All done!
    });

你能不能使用startAsyncmerge来实现带有完整回调的相同效果? - marekful
@marekful 那不是完全一样的吗,只是增加了 Rx 初始化的开销吗? - Madara's Ghost
感谢回复,我已经使用 Promises 运行它了,特别是希望将其转换为 RX。 图像只是一个 <img /> 标签,当加载完成时会触发 onload 事件。也许我需要编写一个自定义观察器? - silverfighter
@silverfighter 看看其他答案中的可观察示例。基本上,您为每个图像创建一个小的可观察对象,然后将它们合并成一个更大的可观察对象。 - Madara's Ghost
@silverfighter,混合使用 Rx 和 promises 完全没有问题。Rx 支持这种用法。 - Benjamin Gruenbaum
显示剩余4条评论

1
function loadImage(url){
    var img = new Image;
    img.src = url;
    var o = new Rx.Subject();
    img.onload = function(){ o.onNext(img); o.onCompleted(); };
    img.onerror = function(e){ o.onError(e); }; // no fromEvent for err handling
    return o;
}

var imageUrls = ['url1', 'url2', 'url3'];
var joined = Rx.Observable.merge(imageUrls.map(loadImage));

// consume one by one:
joined.subscribe(function(item){
    // wait for item
});

joined.toArray().subscribe(function(arr){
    // access results array in arr
});

或者简单地说:
var imageUrls = ['url1', 'url2', 'url3'];
fromArray(imageUrls).map(url => {
    var img = new Image;
    img.src = url;
    return fromEvent(img, "load");
}).toArray().subscribe(function(arr){
    // access results here
});

啊哈,基本上与 Promises 版本非常相似。+1。 - Madara's Ghost
@MadaraUchiha 我刚才在底部添加了一个“糖”版本。 - Benjamin Gruenbaum
使用Subject是过度设计,而且在出现错误的情况下,您将无法在loadImage结果上使用retry运算符。使用merge来连接结果也有点不对 - 您将按照加载顺序获取图像,但不是按照初始URL列表的顺序。 - Bogdan Savluk
@BogdanSavluk 我提供了一个带有主题的答案和一个不带主题的答案,使用 fromEvent 进行错误处理。您可以以一种使其易于重试的方式包装 loadImage,但这并不是真正重要的。在实践中,我最可能使用 fromEvent 代码。顺序不重要,如果重要,我会使用 reduceflatMap,因为 OP 可以始终在结果上使用 .src 并知道哪个是哪个。 - 关键部分是 .toArray,它将 n 个事件序列转换为一个 n 结果数组的事件。 - Benjamin Gruenbaum
看起来 fromEvent 不会等待回调,所以它会返回一个具有未定义源的 AnonymousObservable。fromCallback 有点有效,但是返回的是回调而不是图像 - 你能确认一下还是我误解了什么? - silverfighter

1
这里的其他基于RX的解决方案对我来说并没有真正起作用。Bogdan Savluk的版本根本不起作用。Benjamin Gruenbaum的版本会等到图像加载完成后才开始加载下一个图像,因此速度非常慢(如果我错了请纠正我)。这是我的解决方案,它只比较已加载的图像数量和总图像数量,如果它们相等,则返回的Observable的onNext()方法将被调用,并将图像数组作为参数传递:
var imagesLoaded = function (sources) {

  return Rx.Observable.create(function (observer) {

    var numImages = sources.length
    var loaded = 0
    var images = []

    function onComplete (img) {
      images.push(img)
      console.log('loaded: ', img)

      loaded += 1
      if (loaded === numImages) {
        observer.onNext(images)
        observer.onCompleted()
      }
    }

    sources.forEach(function (src) {
      var img = new Image()
      img.onload = function () {
        onComplete(img)
      }
      console.log('add src: ' + src)
      img.src = src
      if (img.complete) {
        img.onload = null
        onComplete(img)
      }

    })

  })

}

使用方法:

console.time('load images'); // start measuring execution time

imagesLoaded(sources)
  // use flatMap to get the individual images
  // .flatMap(function (x) {
  //   return Rx.Observable.from(x)
  // })

  .subscribe(function (x) {
    console.timeEnd('load images'); // see how fast this was
    console.log(x)
  })

1

以下是使用RxJS加载图像的Angular / Typescript版本:

import { Observable, Observer } from "rxjs"; 

public loadImage(imagePath: string): Observable<HTMLImageElement> {
  return Observable.create((observer: Observer<HTMLImageElement>) => {
    var img = new Image();
    img.src = imagePath;
    img.onload = () => {
      observer.next(img);
      observer.complete();
    };
    img.onerror = err => {
      observer.error(err);
    };
  });
}

我建议添加以下代码以释放内存:.pipe( finalize(() => { URL.revokeObjectURL(image.src); }), ) - Yaroslav Larin

0

这里有一个更好的实现,如果您取消订阅 Observable,则会取消加载图像 https://stackblitz.com/edit/rxjs-loadimage?file=index.ts

import { Observable, Subscriber } from "rxjs";

/**
 * RxJS Observable of loading image that is cancelable
 */
function loadImage(
  url: string,
  crossOrigin?: string
): Observable<HTMLImageElement> {
  return new Observable(function subscriber(subscriber) {
    let img = new Image();
    img.onload = function onload() {
      subscriber.next(img);
      subscriber.complete();
    };
    img.onerror = function onerror(err: Event | string) {
      subscriber.error(err);
    };
    // data-urls appear to be buggy with crossOrigin
    // https://github.com/kangax/fabric.js/commit/d0abb90f1cd5c5ef9d2a94d3fb21a22330da3e0a#commitcomment-4513767
    // see https://code.google.com/p/chromium/issues/detail?id=315152
    //     https://bugzilla.mozilla.org/show_bug.cgi?id=935069
    // crossOrigin null is the same as not set.
    if (
      url.indexOf("data") !== 0 &&
      crossOrigin !== undefined &&
      crossOrigin !== null
    ) {
      img.crossOrigin = crossOrigin;
    }
    // IE10 / IE11-Fix: SVG contents from data: URI
    // will only be available if the IMG is present
    // in the DOM (and visible)
    if (url.substring(0, 14) === "data:image/svg") {
      // TODO: Implement this :)
      // img.onload = null;
      // fabric.util.loadImageInDom(img, onLoadCallback);
    }
    img.src = url;
    return function unsubscribe() {
      img.onload = img.onerror = undefined;
      if (!img.complete) {
        img.src = "";
      }
      img = undefined;
    };
  });
}

// Example
const cacheBurst = new Date().getTime();
const imgUrl = `https://i.pinimg.com/originals/36/0c/62/360c628d043b2461d011d0b7f9b4d880.jpg?nocache=${cacheBurst}`;

const s = loadImage(imgUrl).subscribe(
  img => {
    console.log("Img", img);
  },
  err => {
    console.log("Err", err);
  }
);

setTimeout(() => {
  // uncomment to check how canceling works
  // s.unsubscribe();
}, 100);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接