ngRx 状态更新和 Effects 执行顺序
ngRx state update and Effects execution order
我对这个问题有自己的看法,但最好仔细检查并确定。感谢您的关注和尝试提供帮助。这是:
想象一下,我们正在分派一个动作,该动作会触发一些状态更改并且还附加了一些效果。所以我们的代码必须做两件事——改变状态和做一些副作用。但是这些任务的顺序是什么?我们是同步做的吗?我相信首先,我们改变状态然后产生副作用,但是否有可能在这两个任务之间发生其他事情?像这样:我们改变状态,然后在我们之前做的 HTTP 请求上得到一些响应并处理它,然后做副作用。
[edit:] 我决定在这里添加一些代码。而且我也简化了很多。
州:
export interface ApplicationState {
loadingItemId: string;
items: {[itemId: string]: ItemModel}
}
操作:
export class FetchItemAction implements Action {
readonly type = 'FETCH_ITEM';
constructor(public payload: string) {}
}
export class FetchItemSuccessAction implements Action {
readonly type = 'FETCH_ITEM_SUCCESS';
constructor(public payload: ItemModel) {}
}
减速器:
export function reducer(state: ApplicationState, action: any) {
const newState = _.cloneDeep(state);
switch(action.type) {
case 'FETCH_ITEM':
newState.loadingItemId = action.payload;
return newState;
case 'FETCH_ITEM_SUCCESS':
newState.items[newState.loadingItemId] = action.payload;
newState.loadingItemId = null;
return newState;
default:
return state;
}
}
效果:
@Effect()
FetchItemAction$: Observable<Action> = this.actions$
.ofType('FETCH_ITEM')
.switchMap((action: FetchItemAction) => this.httpService.fetchItem(action.payload))
.map((item: ItemModel) => new FetchItemSuccessAction(item));
这就是我们调度 FetchItemAction 的方式:
export class ItemComponent {
item$: Observable<ItemModel>;
itemId$: Observable<string>;
constructor(private route: ActivatedRoute,
private store: Store<ApplicationState>) {
this.itemId$ = this.route.params.map(params => params.itemId);
itemId$.subscribe(itemId => this.store.dispatch(new FetchItemAction(itemId)));
this.item$ = this.store.select(state => state.items)
.combineLatest(itemId$)
.map(([items, itemId]: [{[itemId: string]: ItemModel}]) => items[itemId])
}
}
期望的场景:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
switchMap operator in our effect cancells previous request_1 and makes request_2;
get the item_2 in response;
store it under key itemId_2 and make loadingItemId = null.
糟糕的场景:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
we receive the response_1 before we made the new request_2 but after loadingItemId changed;
we store the item_1 from the response_1 under the key itemId_2;
make loadingItemId = null;
only here our effect works and we make request_2;
get item_2 in the response_2;
try to store it under key null and get an error
所以问题很简单,糟糕的情况是否真的会发生?
So our code has to do 2 things - change state and do some side
effects. But what is the order of these tasks? Are we doing them
synchronously?
假设我们分派操作 A。我们有一些处理操作 A 的 reducer。这些将按照它们在传递给 StoreModule.provideStore() 的对象中指定的顺序被调用。然后听动作 A 的副作用接下来会触发。是的,是同步的。
I believe that first, we change state and then do the side effect, but
is there a possibility, that between these two tasks might happen
something else? Like this: we change state, then get some response on
HTTP request we did previously and handle it, then do the side
effects.
我从去年年中开始就一直在使用 ngrx,但我从未观察到这种情况。我发现,每次调度一个动作时,它都会经历首先由 reducer 处理然后由副作用处理的整个循环,然后再处理下一个动作。
我认为情况必须如此,因为 redux(ngrx 是从它演变而来的)在其主页上将自己标榜为可预测的状态容器。通过允许发生不可预测的异步操作,您将无法预测任何事情,redux 开发工具也不会很有用。
已编辑 #1
所以我刚刚做了一个测试。我 运行 一个动作 'LONG' 然后副作用会 运行 一个需要 10 秒的操作。与此同时,我能够继续使用 UI,同时向州发送更多消息。最后 'LONG' 的效果完成并发送 'LONG_COMPLETE'。我错了关于减速器和副作用是 t运行saction.
也就是说,我认为预测正在发生的事情仍然很容易,因为所有状态更改仍然是 t运行 的作用。这是一件好事,因为我们不希望 UI 在等待长时间的 运行ning api 调用时阻塞。
已编辑#2
所以如果我理解正确的话,你问题的核心是关于 switchMap 和副作用。基本上你问的是,如果响应在我 运行 缩减程序代码的那一刻返回,然后 运行 switchMap 的副作用取消第一个请求。
我想出了一个我认为可以回答这个问题的测试。我设置的测试是创建 2 个按钮。一只叫快,一只叫长。 Quick 将调度 'QUICK',Long 将调度 'LONG'。监听 Quick 的减速器将立即完成。监听 Long 的减速器需要 10 秒才能完成。
我设置了一个同时收听 Quick 和 Long 的单一副作用。这假装通过使用 'of' 来模拟 api 调用,让我从头开始创建一个可观察对象。然后,这将等待 5 秒(使用 .delay),然后再发送 'QUICK_LONG_COMPLETE'.
@Effect()
long$: Observable<Action> = this.actions$
.ofType('QUICK', 'LONG')
.map(toPayload)
.switchMap(() => {
return of('').delay(5000).mapTo(
{
type: 'QUICK_LONG_COMPLETE'
}
)
});
在我的测试过程中,我点击了快速按钮,然后立即点击了长按钮。
事情是这样的:
- 单击了快捷按钮
- 'QUICK'出动
- 副作用会启动一个将在 5 秒内完成的可观察对象。
- 点击长按钮
- 'LONG'出动
- Reducer 处理 LONG 需要 10 秒。在第 5 秒标记处,来自副作用的原始可观察对象完成但未分派 'QUICK_LONG_COMPLETE'。又过了 5 秒。
- 听 'LONG' 的副作用做了一个 switchmap 取消了我的第一个副作用。
- 5 秒过去,'QUICK_LONG_COMPLETE' 被调度。
因此 switchMap 确实取消了,你的坏情况永远不会发生。
我对这个问题有自己的看法,但最好仔细检查并确定。感谢您的关注和尝试提供帮助。这是:
想象一下,我们正在分派一个动作,该动作会触发一些状态更改并且还附加了一些效果。所以我们的代码必须做两件事——改变状态和做一些副作用。但是这些任务的顺序是什么?我们是同步做的吗?我相信首先,我们改变状态然后产生副作用,但是否有可能在这两个任务之间发生其他事情?像这样:我们改变状态,然后在我们之前做的 HTTP 请求上得到一些响应并处理它,然后做副作用。
[edit:] 我决定在这里添加一些代码。而且我也简化了很多。
州:
export interface ApplicationState {
loadingItemId: string;
items: {[itemId: string]: ItemModel}
}
操作:
export class FetchItemAction implements Action {
readonly type = 'FETCH_ITEM';
constructor(public payload: string) {}
}
export class FetchItemSuccessAction implements Action {
readonly type = 'FETCH_ITEM_SUCCESS';
constructor(public payload: ItemModel) {}
}
减速器:
export function reducer(state: ApplicationState, action: any) {
const newState = _.cloneDeep(state);
switch(action.type) {
case 'FETCH_ITEM':
newState.loadingItemId = action.payload;
return newState;
case 'FETCH_ITEM_SUCCESS':
newState.items[newState.loadingItemId] = action.payload;
newState.loadingItemId = null;
return newState;
default:
return state;
}
}
效果:
@Effect()
FetchItemAction$: Observable<Action> = this.actions$
.ofType('FETCH_ITEM')
.switchMap((action: FetchItemAction) => this.httpService.fetchItem(action.payload))
.map((item: ItemModel) => new FetchItemSuccessAction(item));
这就是我们调度 FetchItemAction 的方式:
export class ItemComponent {
item$: Observable<ItemModel>;
itemId$: Observable<string>;
constructor(private route: ActivatedRoute,
private store: Store<ApplicationState>) {
this.itemId$ = this.route.params.map(params => params.itemId);
itemId$.subscribe(itemId => this.store.dispatch(new FetchItemAction(itemId)));
this.item$ = this.store.select(state => state.items)
.combineLatest(itemId$)
.map(([items, itemId]: [{[itemId: string]: ItemModel}]) => items[itemId])
}
}
期望的场景:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
switchMap operator in our effect cancells previous request_1 and makes request_2;
get the item_2 in response;
store it under key itemId_2 and make loadingItemId = null.
糟糕的场景:
User clicks on itemUrl_1;
we store itemId_1 as loadingItemId;
make the request_1;
user clicks on itemUrl_2;
we store itemId_2 as loadingItemId;
we receive the response_1 before we made the new request_2 but after loadingItemId changed;
we store the item_1 from the response_1 under the key itemId_2;
make loadingItemId = null;
only here our effect works and we make request_2;
get item_2 in the response_2;
try to store it under key null and get an error
所以问题很简单,糟糕的情况是否真的会发生?
So our code has to do 2 things - change state and do some side effects. But what is the order of these tasks? Are we doing them synchronously?
假设我们分派操作 A。我们有一些处理操作 A 的 reducer。这些将按照它们在传递给 StoreModule.provideStore() 的对象中指定的顺序被调用。然后听动作 A 的副作用接下来会触发。是的,是同步的。
I believe that first, we change state and then do the side effect, but is there a possibility, that between these two tasks might happen something else? Like this: we change state, then get some response on HTTP request we did previously and handle it, then do the side effects.
我从去年年中开始就一直在使用 ngrx,但我从未观察到这种情况。我发现,每次调度一个动作时,它都会经历首先由 reducer 处理然后由副作用处理的整个循环,然后再处理下一个动作。
我认为情况必须如此,因为 redux(ngrx 是从它演变而来的)在其主页上将自己标榜为可预测的状态容器。通过允许发生不可预测的异步操作,您将无法预测任何事情,redux 开发工具也不会很有用。
已编辑 #1
所以我刚刚做了一个测试。我 运行 一个动作 'LONG' 然后副作用会 运行 一个需要 10 秒的操作。与此同时,我能够继续使用 UI,同时向州发送更多消息。最后 'LONG' 的效果完成并发送 'LONG_COMPLETE'。我错了关于减速器和副作用是 t运行saction.
也就是说,我认为预测正在发生的事情仍然很容易,因为所有状态更改仍然是 t运行 的作用。这是一件好事,因为我们不希望 UI 在等待长时间的 运行ning api 调用时阻塞。
已编辑#2
所以如果我理解正确的话,你问题的核心是关于 switchMap 和副作用。基本上你问的是,如果响应在我 运行 缩减程序代码的那一刻返回,然后 运行 switchMap 的副作用取消第一个请求。
我想出了一个我认为可以回答这个问题的测试。我设置的测试是创建 2 个按钮。一只叫快,一只叫长。 Quick 将调度 'QUICK',Long 将调度 'LONG'。监听 Quick 的减速器将立即完成。监听 Long 的减速器需要 10 秒才能完成。
我设置了一个同时收听 Quick 和 Long 的单一副作用。这假装通过使用 'of' 来模拟 api 调用,让我从头开始创建一个可观察对象。然后,这将等待 5 秒(使用 .delay),然后再发送 'QUICK_LONG_COMPLETE'.
@Effect()
long$: Observable<Action> = this.actions$
.ofType('QUICK', 'LONG')
.map(toPayload)
.switchMap(() => {
return of('').delay(5000).mapTo(
{
type: 'QUICK_LONG_COMPLETE'
}
)
});
在我的测试过程中,我点击了快速按钮,然后立即点击了长按钮。
事情是这样的:
- 单击了快捷按钮
- 'QUICK'出动
- 副作用会启动一个将在 5 秒内完成的可观察对象。
- 点击长按钮
- 'LONG'出动
- Reducer 处理 LONG 需要 10 秒。在第 5 秒标记处,来自副作用的原始可观察对象完成但未分派 'QUICK_LONG_COMPLETE'。又过了 5 秒。
- 听 'LONG' 的副作用做了一个 switchmap 取消了我的第一个副作用。
- 5 秒过去,'QUICK_LONG_COMPLETE' 被调度。
因此 switchMap 确实取消了,你的坏情况永远不会发生。