[英]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.