Очевидно, здесь нет «виновника».
Это поведение является результатом того, как NgRx использует Rx JS 'сущностей.
Сущность Store
является observable , для которого source
установлено на state$
(объект State
- где хранятся данные):
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
, , что является BehaviorSubject
, это означает, что подписчик будет включен в список подписчиков , поддерживаемый этим субъектом.
Как вы знаете, BehaviorSubject
отправляет самое последнее значение nexted новому абонент. На это указывает первый раз, когда сообщение регистрируется.
Причина, по которой сообщение регистрируется дважды, заключается в том, что всякий раз, когда вы отправляете действие , несколько вещей происходит внутри State
.
Действие будет intercepted
в состоянии, и когда это произойдет, все редукторы будут вызваны с текущим состоянием и текущим действием , в результате чего новое состояние , которое немедленно отправлено его подписчикам :
// 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
, a подписчикам будет отправлено новое состояние, которое должно объяснить, почему вы дважды регистрируете сообщения.
Вы можете удивиться, почему мы не попадаем в бесконечное число l oop. Эти строки предотвращают:
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; вы не получите бесконечную ошибку l oop, потому что asyncScheduler
(от которой наследуется queueScheduler
) будет гарантировать, что если планировщик равен 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
работает внутри, я бы рекомендовал взглянуть на Понимание магии c, стоящей за StoreModule NgRx (@ ngrx / магазин)