Angular 7.2.7中NgRx Effect先于Reducer执行的疑问
NgRx中Reducer与Effect的真实执行顺序说明
官方固定执行顺序(适配你使用的7.2.7版本)
NgRx从源码层面对Action的处理流程做了强约束,顺序不会因为写法变化改变:
- Action被dispatch后,第一步同步执行所有注册的Reducer,基于当前Action和原有State计算出新的全局State,通知所有Store订阅者更新
- 等Reducer执行全量完成、State更新结束后,Action才会被推送到Effect监听的动作流,所有Effect才会开始执行对应逻辑
这个顺序的源码逻辑非常明确:dispatch方法内部会先执行状态合并计算,全部完成后才会把Action实例发送给Effect的监听源,不存在Effect先于Reducer执行的可能。
你观测到"Effect先修改Action属性"的原因
你看到Reducer打印的Action已经携带了修改后的username,本质是两个开发误区叠加产生的错觉,和执行顺序无关:
- Action是JavaScript引用类型,Reducer和Effect拿到的是同一个对象的内存引用,只要任意位置修改了这个对象的属性,所有持有该引用的位置都能拿到修改后的值
- 你在Effect中直接修改了原Action对象的属性,且大概率复用了Action实例:第一次触发UpdateToken时,Reducer会先同步执行,此时Effect还没拿到Action,异步请求也没返回,action.username还是初始值;等异步请求返回、你修改action.username的时候,本次Reducer早就执行完毕了。等你下次复用同一个Action实例触发dispatch时,Reducer拿到的就是上一次异步流程里被修改过属性的Action对象,才会误以为Effect提前执行了。
注意:NgRx的核心设计原则要求Action是不可变对象,创建后禁止修改属性,你当前直接修改原Action属性的写法本身就违反规范,会直接导致状态污染、流程判断异常等问题。
关于"Reducer先执行如何处理异步结果"的逻辑澄清
你对Effect的职责存在一点认知偏差:Effect从来不需要修改当前触发的Action给同一次流程的Reducer使用,它的核心作用是监听动作流、处理异步/副作用逻辑,完成后重新派发新的Action,再走一轮标准的"Reducer处理新Action更新状态"的流程,完全不会和"Reducer同步更新状态"的规则冲突。
规范写法示例
你当前的实现可以调整为符合单向数据流的标准写法,从根源避免引用污染问题:
Effect侧逻辑
不需要关闭dispatch,异步拿到结果后返回新的Action即可,框架会自动完成派发:
@Effect() updateToken$ = this.actions$.pipe( ofType<UpdateToken>(AuthActionTypes.UpdateToken), mergeMap((action) => this.auth.getUsernameFromToken(action.token).pipe( map(username => new UpdateTokenSuccess({ token: action.token, username: username })), catchError(() => of(new UpdateTokenFail())) ) ) );
Reducer侧逻辑
所有Reducer都只做纯同步的状态计算,不处理任何异步逻辑:
// 处理首次触发更新的动作,同步存token、标记加载状态 case AuthActionTypes.UpdateToken: return { ...state, token: action.token, loading: true }; // 处理Effect异步请求成功后派发的新动作,同步存username、关闭加载状态 case AuthActionTypes.UpdateTokenSuccess: return { ...state, username: action.username, loading: false };
整个流程完全符合设计预期:
- 组件派发UpdateToken → Reducer同步更新token和加载状态 → 组件展示加载态
- Effect监听到UpdateToken,发起请求获取用户名
- 请求成功后Effect派发UpdateTokenSuccess
- Reducer收到新动作,同步更新用户名、关闭加载态 → 组件渲染用户信息
顺序验证方式
你可以通过加同步日志的方式直接验证执行顺序,排除异步干扰:
// Reducer中加日志 case AuthActionTypes.UpdateToken: console.log('Reducer执行', Date.now()); return state; // Effect中写同步逻辑加日志 @Effect({dispatch:false}) testOrder$ = this.actions$.pipe( ofType(AuthActionTypes.UpdateToken), map(() => { console.log('Effect执行', Date.now()); }) )
多次触发后你会发现,Reducer的日志永远排在Effect前面,时间戳也更早,是最直接的顺序证明。
内容的提问来源于stack exchange,提问作者Owen Jones
相关产品推荐
相关产品推荐

