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 确实取消了,你的坏情况永远不会发生。