簡體   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