繁体   English   中英

RxJS Observable:订阅丢失了吗?

[英]RxJS Observable: Subscription lost?

以下两个可观察映射之间有什么区别?

(如果以下代码中的某些内容对您来说很奇怪:它源于一个边做边学的爱好项目;我仍然学习RxJS)

我有一个带有getter和构造函数的组件。 两者都从应用程序的ngrx存储中读取信息并提取字符串( name )。

getter和构造函数之间的唯一区别: getter在HTML中使用,它返回的observable通过async管道发送,而构造函数中的observable映射由订阅使用subscribe 我希望它们都可以随着name的新值变得可用而频繁启动。

但是只有getter以这种方式工作并在HTML中提供async管道,并在其中使用新的name值(每次更改名称时都会调用console.log('A') )。 subscribe订阅的回调只被调用一次: console.log('B')console.log('B!')都被调用一次,而不再被调用。

如何解释这种行为差异?

我的组件的片段:

// 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!', _))
  })
}

附加信息:如果我添加ngOnInit ,则在整个测试期间只调用一次该生命周期钩子。 如果我将订阅从构造函数移动到ngOnInit生命周期钩子,它不会比在构造函数中更好地工作。 完全相同(意外)的行为。 这同样适用于ngAfterViewInit和其他生命周期钩子。

名称的输出更改'some-name' -> 'some-other-name' -> 'some-third-name' -> 'some-fourth-name' -> 'some-fifth-name'

[更新]正如Pace在评论中所建议的,我添加了getter通话记录

[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

do s中由console.log打印的输出的示例内容:

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

似乎subscribe订阅在第一次调用后丢失。 但为什么?

你永远不应该使用这样的吸气剂。 不要从一个getter返回可观察到的。

每次发生变化检测周期时,Angular都会一次又一次取消订阅/订阅(这种情况会发生很多)。

就目前而言,我会为“变化检测”写“CD”

简单演示:

拿一个非常简单的组件:

// 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
  }
}

您将看到在控制台中getting a new value ,并且每次单击“单击以触发更改检测”按钮(click)其中已注册(click)事件(click) ,它将触发新的CD周期。

而且,当您点击该按钮时,您会看到两次getting a new value (两次是因为我们不处于生产模式,Angular执行2个CD周期以确保变量在第一次和第二次更改检测之间没有变化,这可能会导致问题,但这是另一个故事)。

可观察的一点是,它可以长时间保持开放 ,你应该利用它。 为了重构前面的代码以保持订阅打开并避免再次取消订阅/订阅,我们可以摆脱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
  }
}

现在,无论您点击按钮多少次,您都会看到一个且只有一个getting a new value (直到observable发出一个新的值),但更改检测不会触发新的订阅。

这是Stackblitz的现场演示,所以你可以玩,看看console.log发生=) https://stackblitz.com/edit/angular-e42ilu

编辑:一个getter是一个函数,因此,Angular必须在每张CD上调用它来检查是否有来自它的新值应该在视图中更新。 这花费了很多,但它是框架的原理和“魔力”。 这也是为什么你应该避免在可能在每张CD上触发的功能中运行密集CPU任务的原因。 如果它是一个纯函数(相同的输入相同输出并且没有副作用),请使用管道,因为默认情况下它们被认为是“纯”并缓存结果。 对于相同的参数,它们只在管道中运行一次函数,缓存结果,然后立即返回结果而不再运行该函数。

ngrx.select()返回的Observable只会在商店中的数据发生变化时触发。

如果你希望在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