RxJS Observable:订阅丢失?
RxJS Observable: Subscription lost?
以下两个可观察映射有什么区别?
(如果你觉得下面的代码有些奇怪:它源于一个边做边学的爱好项目;我还在学习RxJS)
我有一个带有 getter 和构造函数的组件。两者都从应用程序的 ngrx 存储中读取信息并提取字符串 (name
).
getter和构造函数的唯一区别:getter用在HTML和observable中returns 通过 async
管道发送,而构造函数中的可观察映射通过使用 subscribe
的订阅完成。我希望它们都在 name
的新值可用时触发。
但是只有 getter 以这种方式工作,并在 HTML 中提供 async
管道,它与名称的新值一起使用(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 通话记录
[更新] do
根据 Pace
的建议添加
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
中console.log
打印的输出内容示例:
[
{
"name": "some-name"
},
{
"name": "some-other-name"
},
{
"name": "some-third-name"
}
]
似乎 subscribe
订阅在第一次调用后就丢失了。但是为什么?
你永远不应该那样使用 getter。 不要 return 来自getter的Observable。
Angular 会一次又一次地 unsubscribe/subscribe,每次 change detection cycle 发生(经常发生)。
现在我会写 "CD" for "change detection"
简单的演示:
取一个非常简单的组件:
// 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 to trigger change detection",它已经注册了一个 (click)
事件,它会触发一个新的 CD循环.
而且,只要您点击该按钮多次,您就会看到您得到了两次 getting a new value
。
(两次是因为我们不在生产模式下,Angular 执行 2 个 CD 周期以确保变量在第一次和第二次变化检测之间没有变化,这可能会导致问题,但那是另一回事了)。
observable 的要点是它可以长时间保持打开状态,您应该利用这一点。
为了重构之前的代码以保持订阅打开并避免再次出现 unsubscribing/subscribing,我们可以去掉 getter 并声明一个 public 变量(可由模板访问):
// 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 上调用它以检查是否有来自它的新值应该在视图中更新。这成本很高,但这是框架的原理和"magic"。这也是为什么您应该避免 运行ning 密集 CPU 功能中可能在每张 CD 上触发的任务。如果它是纯函数(相同的输入相同的输出并且没有副作用),请使用管道,因为默认情况下它们被认为是 "pure" 并缓存结果。对于相同的参数,他们只会 运行 管道中的函数一次,缓存结果,然后立即 return 结果,而无需再次 运行 调用函数。
从 ngrx.select()
返回的 Observable 只会在商店中的数据发生变化时触发。
如果你希望 Observable 在 initialName
变化时触发,那么我建议将 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!', _))
})
}
以下两个可观察映射有什么区别?
(如果你觉得下面的代码有些奇怪:它源于一个边做边学的爱好项目;我还在学习RxJS)
我有一个带有 getter 和构造函数的组件。两者都从应用程序的 ngrx 存储中读取信息并提取字符串 (name
).
getter和构造函数的唯一区别:getter用在HTML和observable中returns 通过 async
管道发送,而构造函数中的可观察映射通过使用 subscribe
的订阅完成。我希望它们都在 name
的新值可用时触发。
但是只有 getter 以这种方式工作,并在 HTML 中提供 async
管道,它与名称的新值一起使用(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 通话记录
[更新] do
根据 Pace
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
中console.log
打印的输出内容示例:
[
{
"name": "some-name"
},
{
"name": "some-other-name"
},
{
"name": "some-third-name"
}
]
似乎 subscribe
订阅在第一次调用后就丢失了。但是为什么?
你永远不应该那样使用 getter。 不要 return 来自getter的Observable。
Angular 会一次又一次地 unsubscribe/subscribe,每次 change detection cycle 发生(经常发生)。
现在我会写 "CD" for "change detection"
简单的演示:
取一个非常简单的组件:
// 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 to trigger change detection",它已经注册了一个 (click)
事件,它会触发一个新的 CD循环.
而且,只要您点击该按钮多次,您就会看到您得到了两次 getting a new value
。
(两次是因为我们不在生产模式下,Angular 执行 2 个 CD 周期以确保变量在第一次和第二次变化检测之间没有变化,这可能会导致问题,但那是另一回事了)。
observable 的要点是它可以长时间保持打开状态,您应该利用这一点。 为了重构之前的代码以保持订阅打开并避免再次出现 unsubscribing/subscribing,我们可以去掉 getter 并声明一个 public 变量(可由模板访问):
// 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 上调用它以检查是否有来自它的新值应该在视图中更新。这成本很高,但这是框架的原理和"magic"。这也是为什么您应该避免 运行ning 密集 CPU 功能中可能在每张 CD 上触发的任务。如果它是纯函数(相同的输入相同的输出并且没有副作用),请使用管道,因为默认情况下它们被认为是 "pure" 并缓存结果。对于相同的参数,他们只会 运行 管道中的函数一次,缓存结果,然后立即 return 结果,而无需再次 运行 调用函数。
从 ngrx.select()
返回的 Observable 只会在商店中的数据发生变化时触发。
如果你希望 Observable 在 initialName
变化时触发,那么我建议将 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!', _))
})
}