简体   繁体   English

RxJS Observable:订阅丢失了吗?

[英]RxJS Observable: Subscription lost?

What is the difference between the following two observable mappings? 以下两个可观察映射之间有什么区别?

(if something in the following code appears strange to you: it stems from a learning-by-doing hobby project; I still learn RxJS) (如果以下代码中的某些内容对您来说很奇怪:它源于一个边做边学的爱好项目;我仍然学习RxJS)

I have a component with a getter and a constructor. 我有一个带有getter和构造函数的组件。 Both read information from the app's ngrx store and extract a string ( name ). 两者都从应用程序的ngrx存储中读取信息并提取字符串( name )。

The only difference between the getter and the constructor: the getter is used in the HTML and the observable it returns is sent through an async pipe, whereas the observable mapping in the constructor is finished by a subscription using subscribe . getter和构造函数之间的唯一区别: getter在HTML中使用,它返回的observable通过async管道发送,而构造函数中的observable映射由订阅使用subscribe I expect both of them to fire as often as a new value for name becomes available. 我希望它们都可以随着name的新值变得可用而频繁启动。

But instead only the getter works that way and provides the async pipe in the HTML where it is used with new values of name ( console.log('A') is called for every name change). 但是只有getter以这种方式工作并在HTML中提供async管道,并在其中使用新的name值(每次更改名称时都会调用console.log('A') )。 The subscribe subscription's callback is called only once: console.log('B') and console.log('B!') are both called exactly once and never again. subscribe订阅的回调只被调用一次: console.log('B')console.log('B!')都被调用一次,而不再被调用。

How can this difference in behavior be explained? 如何解释这种行为差异?

Snippet from my component: 我的组件的片段:

// getter works exactly as expected:
get name$(): Observable<string> {
  console.log('getter called')
  return this.store
    .select(this.tableName, 'columns')
    .do(_ => console.log('DO (A)', _))
    .filter(_ => !!_)
    .map(_ => _.find(_ => _.name === this.initialName))
    .filter(_ => !!_)
    .map(_ => {
      console.log('A', _.name)
      return _.name
    })
}

// code in constructor seems to lose the subscription after the subscription's first call:
constructor(
  @Inject(TablesStoreInjectionToken) readonly store: Store<TablesState>
) {
  setTimeout(() => {
    this.store
      .select(this.tableName, 'columns')
      .do(_ => console.log('DO (B)', _))
      .filter(_ => !!_)
      .map(_ => _.find(_ => _.name === this.initialName))
      .filter(_ => !!_)
      .map(_ => {
        console.log('B', _.name)
        return _.name
      })
      .subscribe(_ => console.log('B!', _))
  })
}

Additional information: If I add ngOnInit , this life cycle hook is called exactly once during the whole test. 附加信息:如果我添加ngOnInit ,则在整个测试期间只调用一次该生命周期钩子。 If I move the subscription from the constructor to the ngOnInit life cycle hook, it does not work any better than from within the constructor. 如果我将订阅从构造函数移动到ngOnInit生命周期钩子,它不会比在构造函数中更好地工作。 Exactly the same (unexpected) behavior. 完全相同(意外)的行为。 The same applies to ngAfterViewInit and further life cycle hooks. 这同样适用于ngAfterViewInit和其他生命周期钩子。

Output for the name changes 'some-name' -> 'some-other-name' -> 'some-third-name' -> 'some-fourth-name' -> 'some-fifth-name' : 名称的输出更改'some-name' -> 'some-other-name' -> 'some-third-name' -> 'some-fourth-name' -> 'some-fifth-name'

[UPDATE] as suggested by Pace in their comment, I added getter call logs [更新]正如Pace在评论中所建议的,我添加了getter通话记录

[UPDATE] do s added as suggested by Pace [UPDATE] do增补由佩斯作为建议

getter called
DO (A) (3) [{…}, {…}, {…}]
A some-name
DO (B) (3) [{…}, {…}, {…}]
B some-name
B! some-name
getter called
DO (A) (3) [{…}, {…}, {…}]
A some-other-name
getter called
DO (A) (3) [{…}, {…}, {…}]
A some-third-name
getter called
DO (A) (3) [{…}, {…}, {…}]
A some-fourth-name
getter called
DO (A) (3) [{…}, {…}, {…}]
A some-fifth-name

Example content of the output printed by the console.log s in the do s: do s中由console.log打印的输出的示例内容:

[
  {
    "name": "some-name"
  },
  {
    "name": "some-other-name"
  },
  {
    "name": "some-third-name"
  }
]

Seems as if the subscribe subscription gets lost after its first call. 似乎subscribe订阅在第一次调用后丢失。 But why? 但为什么?

You should never use a getter like that. 你永远不应该使用这样的吸气剂。 Do not return an Observable from a getter. 不要从一个getter返回可观察到的。

Angular will unsubscribe/subscribe again and again, everytime a change detection cycle happens (which happens a lot). 每次发生变化检测周期时,Angular都会一次又一次取消订阅/订阅(这种情况会发生很多)。

For now on I'll write "CD" for "change detection" 就目前而言,我会为“变化检测”写“CD”

Simple demo of that: 简单演示:

Take a really simple component: 拿一个非常简单的组件:

// only here to mock a part of the store
const _obsSubject$ = new BehaviorSubject('name 1');

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  get obs$() {
    return _obsSubject$
      .asObservable()
      .pipe(tap(x => console.log('getting a new value')));
  }

  randomFunction() {
    // we don't care about that, it's just
    // to trigger CD from the HTML template
  }
}

You'll see getting a new value in your console, and every time you click on the button "Click to trigger change detection", which has a (click) event registered, it'll trigger a new CD cycle. 您将看到在控制台中getting a new value ,并且每次单击“单击以触发更改检测”按钮(click)其中已注册(click)事件(click) ,它将触发新的CD周期。

And, as many times as you click on that button, you'll see that you are getting twice getting a new value . 而且,当您点击该按钮时,您会看到两次getting a new value (twice is because we're not in production mode and Angular performs 2 CD cycles to make sure the variable has not changed between the first and the second change detection, which might lead to problems but that's another story). (两次是因为我们不处于生产模式,Angular执行2个CD周期以确保变量在第一次和第二次更改检测之间没有变化,这可能会导致问题,但这是另一个故事)。

The point of an observable is that it can remains open for a long time and you should take advantage of that. 可观察的一点是,它可以长时间保持开放 ,你应该利用它。 In order to refactor the previous code to keep the subscription opened and avoid unsubscribing/subscribing again, we can just get rid of the getter and declare a public variable (accessible by the template): 为了重构前面的代码以保持订阅打开并避免再次取消订阅/订阅,我们可以摆脱getter并声明一个公共变量(可由模板访问):

// only here to mock a part of the store
const _obsSubject$ = new BehaviorSubject('name 1');

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  obs$ = _obsSubject$
    .asObservable()
    .pipe(tap(x => console.log('getting a new value')));

  randomFunction() {
    // we don't care about that, it's just
    // to trigger CD from the HTML template
  }
}

And now, no matter how many times you click on the button, you'll see one and only one getting a new value (until the observable emits a new value of course), but the change detection will not trigger a new subscription. 现在,无论您点击按钮多少次,您都会看到一个且只有一个getting a new value (直到observable发出一个新的值),但更改检测不会触发新的订阅。

Here's a live demo on Stackblitz so you can play around and see the console.log happening =) https://stackblitz.com/edit/angular-e42ilu 这是Stackblitz的现场演示,所以你可以玩,看看console.log发生=) https://stackblitz.com/edit/angular-e42ilu

EDIT: A getter is a function, and thus, Angular has to call it on every CD to check if there's a new value coming from it that should be updated in the view. 编辑:一个getter是一个函数,因此,Angular必须在每张CD上调用它来检查是否有来自它的新值应该在视图中更新。 This costs a lot, but it's the principle and the "magic" of the framework. 这花费了很多,但它是框架的原理和“魔力”。 That's also why you should avoid running intensive CPU tasks in function that might be triggered on every CD. 这也是为什么你应该避免在可能在每张CD上触发的功能中运行密集CPU任务的原因。 If it's a pure function (same input same output AND no side effects), use a pipe because they're considered "pure" by default and cache the results. 如果它是一个纯函数(相同的输入相同输出并且没有副作用),请使用管道,因为默认情况下它们被认为是“纯”并缓存结果。 For the same arguments they will run the function in the pipe only once, cache the result and then just return the result instantly without running the function again. 对于相同的参数,它们只在管道中运行一次函数,缓存结果,然后立即返回结果而不再运行该函数。

The Observable returned from ngrx.select() will only fire when the data in the store has changed. ngrx.select()返回的Observable只会在商店中的数据发生变化时触发。

If you want the Observable to fire when initialName changes, then I would recommend converting initialName into an RXJS Subject and using combineLatest : 如果你希望在initialName更改时initialName Observable,那么我建议将initialName转换为RXJS Subject并使用combineLatest

initialNameSubject = new BehaviorSubject<string>('some-name');

constructor(
  @Inject(TablesStoreInjectionToken) readonly store: Store<TablesState>
) {
  setTimeout(() => {
    this.store
      .select(this.tableName, 'columns')
      .combineLatest(this.initialNameSubject)
      .map(([items, initialName]) => items.find(_ => _.name === initialName))
      .filter(_ => !!_)
      .map(_ => {
        console.log('B', _.name)
        return _.name
      })
      .subscribe(_ => console.log('B!', _))
  })
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM