我已经实现了路径解析器,该路径解析器旨在在导航到新路径之前请求一些数据。这是负责将操作分配给商店的代码段:
@Injectable()
export class MyResolver implements Resolve<any> {
constructor(private store: Store<AppState>) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
return this.store
.pipe(
tap(() => {
this.store.dispatch(loadSomeData());
}),
first()
);
}
}
点击适当的路线并触发解析器后,从Redux DevTools中可以看到,在进行导航之前,我的动作已被调度了两次。
好吧,我当时的假设是这是因为ROUTER_REQUEST
和ROUTER_NAVIGATION
在同一路由器转换期间被调度,这导致存储响应两次调度的操作而发出两次。但是,如果我注释掉分派动作的那一行并放在console.log
那里,就像这样:
tap(() => {
// this.store.dispatch(loadSomeData());
console.log('Dispatching action');
})
路由器存储仍将触发ROUTER_REQUEST
和ROUTER_NAVIGATION
动作,但是这次存储只发出一次,并且测试消息仅一次输出到控制台。
动作派遣有何不同?
在这种情况下,ROUTER_REQUEST
和ROUTER_NAVIGATION
动作不重要吗?
这是您可以运行的StackBlitz。
答案 0 :(得分:1)
显然,这里没有“罪魁祸首”。
此行为是NgRx如何使用RxJS的实体的结果。
Store
实体是可观察的,其source
设置为state$
(State
实体-存储数据的实体):< / p>
export class Store<T = object> extends Observable<T>
implements Observer<Action> {
constructor(
state$: StateObservable,
private actionsObserver: ActionsSubject,
private reducerManager: ReducerManager
) {
super();
this.source = state$;
}
}
这意味着每个订阅store
的订阅者(例如:lazy.resolver
-this.store.pipe(...)
)实际上将订阅State
,which is a BehaviorSubject
,这意味着订阅者将属于此主题维护的订阅者列表。
您知道,BehaviorSubject
将最新的下一个值发送给新订户。这是第一次记录消息时指出的。
两次记录该消息的原因是,每当您调度一个动作时,State
内就会发生一些事情。
该动作将处于intercepted
状态,并且当其发生时,所有化简器都将以当前状态和当前动作进行调用,从而导致< strong>新状态,并立即发送给其订户:
// Inside `State.ts`
// Actions stream
const actionsOnQueue$: Observable<Action> = actions$.pipe(
observeOn(queueScheduler)
);
// Making sure everything happens after the reducers have been set up
const withLatestReducer$: Observable<
[Action, ActionReducer<any, Action>]
> = actionsOnQueue$.pipe(withLatestFrom(reducer$));
const seed: StateActionPair<T> = { state: initialState }; // Default state
const stateAndAction$: Observable<{
state: any;
action?: Action;
}> = withLatestReducer$.pipe(
scan<[Action, ActionReducer<T, Action>], StateActionPair<T>>(
reduceState, // Calling the reducers, resulting in a new state
seed
)
);
this.stateSubscription = stateAndAction$.subscribe(({ state, action }) => {
this.next(state); // Send the new state to the subscribers(e.g in lazy.resolver)
scannedActions.next(action); // Send the action so that it can be intercepted by the effects
});
因此,store
最初被订阅时,它将发送其最新的存储值,但是当您调度另一个操作(例如)在tap
的下一个回调中执行时,将处于新状态发送给订阅者,这应该解释您为何两次记录该消息。
您可能想知道为什么我们不会陷入无限循环。这些行可以防止:
const actionsOnQueue$: Observable<Action> = actions$.pipe(
observeOn(queueScheduler)
);
observeOn(queueScheduler)
在这里非常重要。每当调度动作时(例如this.store.dispatch(loadSomeData());
),它都不会立即传递动作(尽管它使用的是queueScheduler
),但它会安排动作,做一些工作(在这种情况下,work
是传递操作的任务)。
但是当您执行this.store.dispatch(loadSomeData());
时,它将再次到达this.store.dispatch(loadSomeData());
,并且到达first()
之前。
我猜这就是为什么被称为 queue Scheduler;您不会遇到无限循环错误,因为asyncScheduler
(继承自queueScheduler
)将确保 scheduler 是 active (一个动作正在执行其工作,例如第一个this.store.dispatch(loadSomeData())
),新到达的动作将被添加到队列中,并且在活动动作完成其工作时将被考虑在内:
const { actions } = this;
if (this.active) {
actions.push(action);
return;
}
let error: any;
this.active = true;
do {
if (error = action.execute(action.state, action.delay)) {
break;
}
} while (action = actions.shift()!); // exhaust the scheduler queue
并且由于您使用的是first()
和take(5)
,因此可以确保在某个动作完成工作之后(例如,将值进一步推入流中),确保队列中的下一个动作获胜不会产生任何效果,因为在这种情况下,在队列中的任何操作都有机会执行其任务之前,会先达到first()
或take(5)
。
如果您想了解有关ngrx/store
内部工作方式的更多信息,建议您查看Understanding the magic behind StoreModule of NgRx (@ngrx/store)