从Observable中获取当前值而不订阅(仅想要一次值)

100

我如何在不订阅Observable的情况下获取当前值? 我只想获取当前值一次,而不是当新值出现时。


在使用HTTP请求时,您想直接从.map获取数据吗? - Pardeep Jain
实际上不是这样的,我有自己制作的Observable(应用程序的状态),通常我使用subscribe并在更改到来时进行操作,但现在我只需要获取当前状态(未来不更新或更改)。 - EricC
take方法对你有用吗? http://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/take.md - Sjoerd
嗯,看起来take方法返回一个Observable,我只想要值(在我的情况下是字符串)... - EricC
你可以在这里查看答案:https://dev59.com/6VoU5IYBdhLWcg3wzZE1#54387211 - Keerati Limkulphong
6个回答

119

快速回答:

...我只想获取当前值一次,而不是随着新值的到来而获取...

您仍将使用subscribe,但使用pipe(take(1)),这样它就会给您一个单一的值。

例如:myObs$.pipe(take(1)).subscribe(value => alert(value));

另请参见:first()take(1)single()之间的比较


更长的回答:

一般规则是只能使用 subscribe() 从可观察对象中获取一个值

(或者如果使用 Angular,可以使用 async 管道)

BehaviorSubject 绝对有它的用处。当我开始使用 RxJS 时,我经常使用 bs.value() 来获取值。随着你的 RxJS 流在整个应用程序中传播(这就是你想要的!),这将变得越来越困难。通常你会看到 .asObservable() 被用来“隐藏”底层类型,以防止某人使用 .value() - 起初这似乎很严厉,但随着时间的推移,你会开始欣赏为什么要这样做。此外,你迟早需要某些不是 BehaviorSubject 的值,而没有办法让它成为 BehaviorSubject

但是回到最初的问题。尤其是如果您不想通过使用 BehaviorSubject 来“作弊”。

更好的方法总是使用 subscribe 来获取值。

obs$.pipe(take(1)).subscribe(value => { ....... })

或者

obs$.pipe(first()).subscribe(value => { ....... })

这两者之间的区别在于,first()会在流已经完成时出错,而take(1)则不会在流已完成或没有立即可用的值时发出任何observable。

注意:即使您正在使用BehaviorSubject,这也被认为是更好的做法。

然而,如果您尝试上述代码,则observable的“value”将被“卡住”在subscribe函数的闭包中,您可能需要在当前作用域中使用它。如果您确实需要解决这个问题,可以采用以下方法:

const obsValue = undefined;
const sub = obs$.pipe(take(1)).subscribe(value => obsValue = value);
sub.unsubscribe();

// we will only have a value here if it was IMMEDIATELY available
alert(obsValue);

需要注意的是,上面的订阅调用不会等待值。如果立即没有可用的内容,则订阅函数将永远不会被调用,并且我故意在那里放置了取消订阅调用以防止它“稍后出现”。

因此,这不仅看起来非常笨拙 - 对于不立即可用的东西(例如来自http调用的结果值),它也无法工作,但实际上它可以使用行为主题(或更重要的是已知为BehaviorSubject的上游内容,或者取两个BehaviorSubject值的combineLatest)。绝对不要做(obs$ as BehaviorSubject)- 呕!

通常情况下,前面的示例仍然被认为是一种不好的做法 - 它很混乱。只有当我想查看一个值是否立即可用并能够检测到它不存在时,我才会使用前面的代码样式。

最佳方法

如果可能的话,您最好尽可能将所有内容保持为observable,并且仅在绝对需要该值时才进行订阅 - 不要尝试将值“提取”到包含范围中,这就是我上面所做的。

例如,假设我们想要制作一份关于我们动物的报告,如果你的动物园是开放的。你可能会认为你想要像这样提取zooOpen$的值:

不好的方法

zooOpen$: Observable<boolean> = of(true);    // is the zoo open today?
bear$: Observable<string> = of('Beary');
lion$: Observable<string> = of('Liony');

runZooReport() {

   // we want to know if zoo is open!
   // this uses the approach described above

   const zooOpen: boolean = undefined;
   const sub = this.zooOpen$.subscribe(open => zooOpen = open);
   sub.unsubscribe();

   // 'zooOpen' is just a regular boolean now
   if (zooOpen) 
   {
      // now take the animals, combine them and subscribe to it
      combineLatest(this.bear$, this.lion$).subscribe(([bear, lion]) => {

          alert('Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion); 
      });
   }
   else 
   {
      alert('Sorry zoo is closed today!');
   }
}

那么这为什么是如此糟糕

  • 如果zooOpen$来自Web服务怎么办?如果zooOpen$是一个http可观察对象,上面的代码将永远无法工作,无论您的服务器有多快 - 您都无法获得值!
  • 如果您想在此函数之外使用此报告。您现在已经将alert锁定到此方法中。如果您必须在其他地方使用报告,则必须复制它!

好的方式

不要试图在函数中访问值,相反,考虑创建一个新的Observable函数,甚至不订阅它!

它返回一个可以在“外部”使用的新的可观察对象。

通过保持所有内容为可观察对象并使用switchMap进行决策,您可以创建新的可观察对象,它们本身可以成为其他可观察对象的来源。

getZooReport() {

  return this.zooOpen$.pipe(switchMap(zooOpen => {

     if (zooOpen) {

         return combineLatest(this.bear$, this.lion$).pipe(map(([bear, lion] => {

                 // this is inside 'map' so return a regular string
                 return "Welcome to the zoo! Today we have a bear called ' + bear + ' and a lion called ' + lion";
              }
          );
      }
      else {

         // this is inside 'switchMap' so *must* return an observable
         return of('Sorry the zoo is closed today!');
      }

   });
 }

以上代码创建了一个新的可观察对象,因此我们可以在其他地方运行它,并在需要时进行更多的管道操作。

 const zooReport$ = this.getZooReport();
 zooReport$.pipe(take(1)).subscribe(report => {
    alert('Todays report: ' + report);
 });

 // or take it and put it into a new pipe
 const zooReportUpperCase$ = zooReport$.pipe(map(report => report.toUpperCase()));

请注意以下内容:
  • 在这种情况下,我只有在绝对需要时才订阅 - 在函数之外
  • 'driving'可观察对象是zooOpen$,它使用switchMap切换到另一个可观察对象,最终返回自getZooReport()
  • 如果zooOpen$发生变化,则会取消所有操作并重新开始第一个switchMap内的操作。了解更多关于switchMap的信息。
  • 注意:在switchMap内部的代码必须返回一个新的可观察对象。您可以使用of('hello')快速创建一个可观察对象,或者返回另一个可观察对象,例如combineLatest
  • 同样地:map必须只返回常规字符串。

当我开始心理记录直到我不得不这样做时,我突然开始编写更具生产力、灵活性、清洁度和可维护性的代码。

另一个最后的提示:如果您在Angular中使用此方法,可以通过使用| async管道来获取上述动物园报告而不需要任何subscribe。这是“在必须订阅之前不要订阅”的实践的绝佳示例。
// in your angular .ts file for a component
const zooReport$ = this.getZooReport();

在你的模板中:

<pre> {{ zooReport$ | async }} </pre>

在这里也可以看到我的答案:

https://dev59.com/6VoU5IYBdhLWcg3wzZE1#54209999

此外为避免混淆,还有一些内容没有提到:

  • tap() 有时可能很有用,可以“从可观察对象中获取值”。 如果您不熟悉该操作符,请阅读相关文档。 RxJS 使用“管道”和 tap() 操作符是一种“进入管道”的方式以查看其中的内容。

使用 .toPromise() / async

请查看https://benlesh.medium.com/rxjs-observable-interop-with-promises-and-async-await-bebb05306875中的“使用toPromise()和async/await将最后一个Observable值作为Promise发出”。


使用 Angular 信号!(Angular 16+)

Angular 16 引入了一个新的 Signals 功能的预览。虽然不是完整的功能,但您已经可以在组件中开始使用 signals。其中一个设计目标就是解决这个“痛点”。

通过 customer = toSignal(customer$, { requireSync: true }),您可以创建一个信号,当可观察值更改时更新,并使用 customer() 访问“立即”值,而无需上述常规样板文件。Angular 自动处理取消订阅可观察对象以实现此目的。

对此的想法:

  • 使用 () 访问信号以获取其值始终是同步的,因此如果您的可观察对象没有值,则会遇到上述某些问题。这就是 { requireSync: true } 的用途(有关更多信息,请查看 API 文档 - 可能会更改)。
  • 我并不建议在需要现有可观察对象的值时创建新的信号。那样有点毫无意义!只有在部分或完全更新组件以使用 signals 时,这种方法才有用。
  • 信号 API 不是最终版本,因此请查看博客以获取最新信息。

8
这非常清晰,是我见过的对这个问题最好的回答。为此你应该受到赞扬,谢谢! - Nmuta
1
@Shem ha 谢谢。我在这个答案上有点过火了! - Simon_Weaver
1
在这种情况下,是否仍然有必要取消订阅? - Tobias Kaufmann
另一种思考方式是,不好的方法很难进行单元测试,而好的方法很容易进行单元测试。 - foxiris
1
@Ganesh 这是一个很好的例子,说明新的信号可能非常适合。当我开始使用Angular时,我的代码中出现了很多bs$.value的情况。最后我总是陷入混乱之中。在最简单的情况下,你的代码可能今天可以使用行为主题,但明天你需要将其与其他输入或数据结合起来,突然间就不能再使用.value了。虽然它能工作,但感觉笨拙,我再也不用它了。 - Simon_Weaver
显示剩余4条评论

63
你需要使用 BehaviorSubject
  • BehaviorSubject 类似于 ReplaySubject,但它仅记住最后一次发布的值。
  • BehaviorSubject 还要求您提供 T 的默认值。这意味着所有订阅者将立即收到一个值(除非它已经完成)。
它将为您提供 Observable 发布的最新值。
BehaviorSubject 提供了一个名为 value 的 getter 属性,用于获取通过它传递的最新值。

StackBlitz

  • 在这个例子中,值'a'被写入到控制台:

//Declare a Subject, you'll need to provide a default value.
const subject: BehaviorSubject<string> = new BehaviorSubject("a");

使用方法:

console.log(subject.value); // will print the current value

隐藏Subject,仅暴露其值

如果您想要隐藏您的BehaviorSubject并仅从服务中公开其值,您可以使用以下getter。

export class YourService {
  private subject = new BehaviorSubject('random');

  public get subjectValue() {
    return this.subject.value;
  }
}

我有一个像这样的状态(使用TypeScript):myState:EventEmitter<string> = new EventEmitter();,所以我必须使用BehaviorSubject而不是EventEmitter,对吗?类似于myState:BehaviorSubject<string> = new BehaviorSubject('default state'); - EricC
是的,没错。EventEmitter 使用简单的 Observable。 - Ankit Singh
8
但是BehaviorSubject有'next'方法,因此如果一个类向其客户端公开BehaviorSubject,则这些客户端就可以触发“next”并更新变量。需要的是一种方法,以将BehaviorSubject的值和Observable成员仅暴露给源对象的客户端。有人知道我该如何做到这一点吗? - Neutrino
1
@Neutrino:使用TypeScript,您可以将BehaviorSubject<T>转换为Observable<T>,并仅向其他人公开该可观察对象。这对您是否可以? - user276648
1
我不确定这真的有多大作用。任何人都可以将强制转换对象分配给未类型化的变量,他们仍然可以调用它的“next”方法,不是吗? - Neutrino

11
const value = await this.observableMethod().toPromise();

这实际上是适合我的方法。 - SkogensKonung
这是一篇关于 Promises + RxJS 的好文章:https://benlesh.medium.com/rxjs-observable-interop-with-promises-and-async-await-bebb05306875 - 请查看“使用 toPromise() 和 async/await 发出最后一个 Observable 值作为 Promise”部分。 - Simon_Weaver

1

由于toPromise()方法已经过时,这里介绍一种新的rxjs方法来替代:

let value = await lastValueFrom(someObservable);

或者

let value = await firstValueFrom(someObservable);

如果您不确定某个东西是否已经存在,您可以使用以下方法:
let value = await lastValueFrom(someObservable, {defaultValue: "some value"})

https://indepth.dev/posts/1287/rxjs-heads-up-topromise-is-being-deprecated


0

不确定这是否是您要寻找的内容。可能需要在服务中编写BehaviorSubject。将其声明为私有,并仅公开您设置的值。类似于以下内容:

 @Injectable({
   providedIn: 'root'
 })
  export class ConfigService {
    constructor(private bname:BehaviorSubject<String>){
       this.bname = new BehaviorSubject<String>("currentvalue");
    }

    getAsObservable(){
       this.bname.asObservable();
    }
 }

这样,外部用户只能订阅behaviorSubject并且您可以在服务中设置所需的值。


0
使用 Observable 构造函数创建任何类型的可观察流。构造函数以其参数订阅者函数作为其运行的函数,当可观察对象的 subscribe() 方法执行时,该订阅者函数将被调用。订阅者函数接收一个 Observer 对象,并可以将值发布到观察者的 next() 方法中。试试这个。
@Component({
  selector: 'async-observable-pipe',
  template: '<div><code>observable|async</code>: Time: {{ time | async }} . 
</div>'
})
export class AsyncObservablePipeComponent {
  time = new Observable<string>((observer: Observer<string>) => {
    setInterval(() => observer.next(new Date().toString()), 1000);
  });
}

虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - adiga
1
你还需要什么吗?@adiga - Kshitij

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