ngRx 状态更新和效果执行顺序

Posted

技术标签:

【中文标题】ngRx 状态更新和效果执行顺序【英文标题】:ngRx state update and Effects execution order 【发布时间】:2017-12-04 19:46:29 【问题描述】:

我对这个问题有自己的看法,但最好仔细检查并确定。感谢您的关注并试图提供帮助。这里是:

想象一下,我们正在调度一个动作,该动作触发了一些状态变化,并且还附加了一些效果。所以我们的代码必须做两件事——改变状态和做一些副作用。但是这些任务的顺序是什么?我们是否同步进行?我相信首先,我们改变状态然后做副作用,但是有没有可能,这两个任务之间可能会发生其他事情?像这样:我们改变状态,然后对我们之前做的 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

所以问题很简单,糟糕的情况是否真的会发生?

【问题讨论】:

【参考方案1】:

所以我们的代码必须做两件事 - 改变状态并做一些事情 效果。但是这些任务的顺序是什么?我们在做吗 同步?

假设我们调度动作 A。我们有一些处理动作 A 的 reducer。它们将按照传递给 StoreModule.provideStore() 的对象中指定的顺序被调用。然后接下来会触发听动作 A 的副作用。是的,它是同步的。

我相信首先,我们改变状态,然后做副作用,但是 有没有可能,这两个任务之间可能会发生 别的东西?像这样:我们改变状态,然后得到一些响应 我们之前做的HTTP请求并处理它,然后做旁边 效果。

自去年年中以来我一直在使用 ngrx,但我从未观察到这种情况。我发现每次调度一个动作时,它都会经历首先由减速器处理,然后是副作用,然后再处理下一个动作的整个周期。

我认为这必须是这样,因为 redux(从 ngrx 演变而来)在其主页上将自己标榜为可预测的状态容器。如果允许发生不可预测的异步操作,您将无法预测任何事情,redux 开发工具也不会很有用。

已编辑 #1

所以我只是做了一个测试。我运行了一个动作“LONG”,然后副作用将运行一个需要 10 秒的操作。与此同时,我能够继续使用 UI,同时向该州进行更多调度。最后,“LONG”的效果完成并发送了“LONG_COMPLETE”。我错了减速器和副作用是交易。

也就是说,我认为仍然很容易预测正在发生的事情,因为所有状态更改仍然是事务性的。这是一件好事,因为我们不希望 UI 在等待长时间运行的 api 调用时阻塞。

已编辑 #2

因此,如果我正确理解这一点,那么您问题的核心是关于 switchMap 和副作用。基本上你是在问,如果在我运行 reducer 代码时响应返回怎么办,然后使用 switchMap 运行副作用以取消第一个请求。

我想出了一个测试,我相信它可以回答这个问题。我设置的测试是创建 2 个按钮。一个叫Quick,一个叫Long。 Quick 将发送“QUICK”,Long 将发送“LONG”。听 Quick 的 reducer 会立即完成。听 Long 的 reducer 需要 10 秒才能完成。

我设置了一个同时收听 Quick 和 Long 的副作用。这假装通过使用“of”来模拟 api 调用,让我从头开始创建一个 observable。然后,这将等待 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 秒标记处,来自副作用的原始 observable 完成,但没有调度“QUICK_LONG_COMPLETE”。又过了 5 秒。 听“LONG”的副作用会产生一个取消我的第一个副作用的开关图。 5 秒过去了,“QUICK_LONG_COMPLETE”被调度。

因此 switchMap 确实取消了,你的坏情况不应该发生。

【讨论】:

感谢您的回复!是的,我提到的最后一种情况非常不受欢迎 但是你是怎么做出这个效果的?你添加了 setTimeout() 还是什么?我的使用场景是这样的: 1. 用户点击一个 item 2. 我们发送一个 action A,payload 是这个 item 的 id 3. 一些 reducer 将这个 item id 存储在加载状态(state.loading.id) 4. 效果使用这个 id 发出一个 http 请求(我们订阅一个动作并执行一个 switchMap) 5. 我们接收响应并调度动作 A_complete 6. 使用一些减速器,我们将接收到的项目存储到如下状态:state.items[state. loading.id] = response 也做 state.loading.id = null 我看到的唯一问题是我们可能会更改 state.loading.id 但接收到先前的响应比我们发出另一个请求要快。在这种情况下,在我们的状态下,最后一个 id 将是我们的上一个项目 我的效果是 of('random').delay(10000)。我没有确切地关注问题所在。也许你可以用 UI 来解释。我不清楚当一个项目加载时是否有一个微调器(或其他加载器),或者每个项目加载是否有多个独立的微调器。另外,您的问题与微调器有关还是与其他一些数据损坏有关? 非常感谢您的帮助!对我来说意义重大。我对该主题进行了重大更新,添加了代码并描述了所需和不需要的场景。问题是我不确定它是否真的会发生。

以上是关于ngRx 状态更新和效果执行顺序的主要内容,如果未能解决你的问题,请参考以下文章

在调度一个动作和 NgRx 效果之前执行多个 API 调用

sql语句的执行顺序是啥,为啥下面这两个sql执行的结果是一样的

java多线程笔记--顺序执行

VueJS 调度函数的执行顺序导致问题

Promise 状态的表现与变化以及执行顺序

Promise 状态的表现与变化以及执行顺序