状态管理
Angular 没有强制单一状态库:服务 + DI、Signal、RxJS 即可覆盖多数场景;大型应用可选用 NgRx。见 Signal 基础、依赖注入。
核心概念
Angular 的「状态」通常分三个层次:
- 组件内状态:只影响单个组件的 UI 状态(展开/收起、当前 tab),用
signal即可。 - 跨组件共享状态:多个不直接父子关系的组件需要同一份数据(购物车、当前用户),用 可注入服务 + Signal 承载,借助 Angular DI 的单例特性天然共享。
- 全局/复杂业务状态:涉及多步骤流程、时间旅行调试、严格的状态变更审计,用 NgRx 等 Redux 风格方案。
选型的关键是不要过度设计:多数中台/后台系统用第 2 层(服务 + Signal)就足够,只有状态流转复杂、团队规模大、需要强约束时才引入 NgRx。
选型简表
| 场景 | 建议 |
|---|---|
| 组件内 UI 状态 | signal / computed |
| 跨组件共享(中等复杂度) | @Injectable({ providedIn: 'root' }) 服务 + signal |
| 异步服务端数据 | HttpClient + toSignal / async 管道 |
| 复杂事件溯源、时间旅行调试 | NgRx Store |
服务作为状态容器
@Injectable({ providedIn: "root" })
export class CartService {
private items = signal<CartItem[]>([]);
readonly itemsReadonly = this.items.asReadonly();
readonly total = computed(() => this.items().reduce((s, i) => s + i.price, 0));
add(item: CartItem) {
this.items.update((list) => [...list, item]);
}
remove(id: string) {
this.items.update((list) => list.filter((i) => i.id !== id));
}
clear() {
this.items.set([]);
}
}
组件通过 inject(CartService) 读取,模板使用 cart.total():
@Component({
standalone: true,
template: `
<p>共 {{ cart.itemsReadonly().length }} 件,合计 {{ cart.total() }}</p>
<button (click)="cart.clear()">清空</button>
`,
})
export class CartSummaryComponent {
cart = inject(CartService);
}
要点:内部可写状态用 private items = signal(...),只暴露 asReadonly() 或 computed,防止外部组件绕过 add/remove 直接改数据,保持变更路径可追踪。
跨模块共享与生命周期
providedIn: 'root' 的服务在整个应用中是单例,只要不特意在某个组件/路由层重新 provide,所有注入点拿到的是同一份实例——这是 Angular DI 替代「全局 store」的关键机制。若需要每个路由/懒加载模块各自一份状态,则在该路由的 providers 数组里重新声明该 Service,形成子作用域实例。
export const routes: Routes = [
{
path: "checkout",
providers: [CartService], // 该路由子树内单独一份 CartService,退出后随路由销毁
loadComponent: () => import("./checkout.component").then((m) => m.CheckoutComponent),
},
];
NgRx(可选)
NgRx 实现 Redux 风格:Store、Actions、Reducers、Effects(副作用)。适合多团队协作、强约束数据流;小项目可能过重。
// 简化示意:Action、Reducer、Selector
export const increment = createAction("[Counter] Increment");
export const counterReducer = createReducer(0, on(increment, (state) => state + 1));
export const selectCount = createFeatureSelector<number>("count");
@Component({
template: `<p>{{ count() }}</p><button (click)="inc()">+1</button>`,
})
export class CounterComponent {
private store = inject(Store);
count = this.store.selectSignal(selectCount); // NgRx 提供 Signal 化 selector
inc() {
this.store.dispatch(increment());
}
}
官方也提供 NgRx SignalStore,与 Signal API 更贴近,比传统 Store + Reducer + Effects 写法更简洁,适合不需要严格 Action/Reducer 分层但仍想要结构化状态管理的团队。
与 Vue Pinia / React Redux 对照
| Angular | Vue | React | |
|---|---|---|---|
| 轻量方案 | Service + signal | Pinia | Zustand / Context |
| 重量方案 | NgRx | Vuex(旧) | Redux Toolkit |
| 派生状态 | computed | computed | useMemo / selector |
| DevTools | NgRx Store DevTools | Pinia DevTools | Redux DevTools |
最佳实践
- 优先用服务 + Signal,只有确认业务复杂度需要(多步骤流程、跨团队协作、严格审计)才引入 NgRx。
- Service 内部状态尽量私有,只暴露
readonly的 signal/computed,写操作封装成有业务含义的方法(add、checkout)。 - 服务端数据(HTTP 响应)用
toSignal转换为 Signal,避免手动subscribe后再赋值造成的内存泄漏风险。 - 若引入 NgRx,先从 SignalStore 或局部 feature store 开始,不必一次性把全应用状态搬进全局 Store。
- 路由级临时状态(如某个多步表单向导)用路由
providers声明子作用域服务,随路由销毁自动清理。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 服务里的数据「跨路由丢失」 | 服务被声明在懒加载模块 providers 里形成了新实例 | 确认服务应是 root 单例还是路由子作用域,按需选择 |
| 组件外部直接改了服务内部数组 | 暴露了可写 signal 而非 asReadonly()/computed | 只导出只读引用,写操作走方法 |
| NgRx 引入后代码量剧增、维护成本高 | 业务并不复杂却套用完整 Redux 分层 | 评估是否降级为 Service + Signal 或 SignalStore |
computed 没有更新 | 依赖的 signal 未被在 computed 函数体内实际读取(如被条件短路跳过) | 确保依赖的 signal 在每次求值路径中都被调用 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| 状态管理模式 | 官方 Signal + 服务 |
| NgRx Store | 英文 |
| NgRx SignalStore | Signal 风格 |
| 依赖注入 | providedIn 作用域 |
相关文章
生态集成
Angular 官方维护路由、HTTP、表单、动画等;UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方/半官方库、常用第三方库、与 Vue/React 生态对照、选型建议与常见坑。
Signal 基础
Signals 是 Angular 16 引入的响应式原语:可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新,涵盖 API、input signal、与变更检测的关系、最佳实践与常见坑。
RxJS 互操作
Angular 内置 RxJS 处理异步流(HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等)。Signal 适合同步/短链路状态;长生命周期流仍常用 Observable。涵盖 async 管道、toSignal/toObservable、常用操作符、最佳实践与常见坑。
表单
Angular 提供 响应式表单(Reactive Forms) 与 模板驱动表单(Template-driven)。复杂表单推荐 Reactive Forms(模型在 TypeScript 中,易测试)。涵盖 FormGroup/FormArray、Typed Forms、自定义校验器、最佳实践与常见坑。
HTTP 与数据
Angular 通过 HttpClient(@angular/common/http)发起 HTTP 请求,返回 RxJS Observable。见 RxJS 互操作、依赖注入。
变更检测
Angular 在数据变化后更新 DOM 的过程称为 变更检测(Change Detection)。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同,有助于性能调优。运行时技巧见 runtime。