使用 RxJS 在 Angular 中进行简单而强大的状态管理

2025-05-24

使用 RxJS 在 Angular 中进行简单而强大的状态管理

TLDR让我们仅使用 RxJS/BehaviorSubject 创建我们自己的状态管理类(受到一些众所周知的状态管理库的启发)。

使用 RxJS BehaviorSubject 管理状态

Angular 中有很多优秀的状态管理库,例如 NgRx、Akita 或 NgXs。它们都有一个共同点:它们基于 RxJS Observable,并且状态存储在一种特殊的 Observable 中:BehaviorSubject。

为什么选择 RxJS Observables?

  • 可观察对象是 Angular 中的“一等公民”。Angular 的许多核心功能都由 RxJS 实现(例如 HttpClient、Forms、Router 等等)。使用可观察对象管理状态可以与 Angular 生态系统的其他部分完美集成。
  • 使用 Observable 对象可以轻松地将状态变化通知给组件。组件可以订阅保存状态的 Observable 对象。这些“State” Observable 对象会在状态发生变化时发出新的值。

BehaviorSubject 有什么特别之处?

  • BehaviorSubject 将其最后发出的值发送给新的/迟到的订阅者
  • 它有一个初始值
  • 可以通过getValue方法访问其当前值
  • next可以使用方法发出新值
  • BehaviorSubject 是多播的:它内部保存着所有订阅者的列表。所有订阅者共享同一个 Observable 执行。当 BehaviorSubject 发出新值时,相同的值会被推送给所有订阅者。

使用 BehaviorSubject 进行我们自己的状态管理

因此,如果所有大型状态管理库都使用 RxJS BehaviorSubject,并且 Angular 开箱即用 RxJS......我们可以仅使用 Angular Services 和 BehaviorSubject 创建自己的状态管理吗?

让我们创建一个简单但功能强大的状态管理类,可以通过 Angular 服务进行扩展。

主要目标是:

  • 能够定义状态接口并设置初始状态
  • 直接使用 API 来更新状态和选择状态:setStateselect
  • 选中状态应以 Observable 形式返回。选中状态发生变化时,Observable 会发出相应信号。
  • 能够ChangeDetectionStrategy.OnPush在我们的组件中使用以获得更好的性能(在此处阅读有关 OnPush 的更多信息:“Angular onPush 变化检测策略综合指南”)。

解决方案:

import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';

export class StateService<T> {
  private state$: BehaviorSubject<T>;
  protected get state(): T {
    return this.state$.getValue();
  }

  constructor(initialState: T) {
    this.state$ = new BehaviorSubject<T>(initialState);
  }

  protected select<K>(mapFn: (state: T) => K): Observable<K> {
    return this.state$.asObservable().pipe(
      map((state: T) => mapFn(state)),
      distinctUntilChanged()
    );
  }

  protected setState(newState: Partial<T>) {
    this.state$.next({
      ...this.state,
      ...newState,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

让我们仔细看看上面的代码:

  • StateService 需要一个T表示状态接口的泛型类型。此类型在扩展 StateService 时传递。
  • get state()返回当前状态快照
  • 构造函数采用初始状态并初始化 BehaviorSubject。
  • selectstate$接受一个回调函数。该函数在发出新状态时被调用。在 RxJS 中map,回调函数将返回一个状态片段。distinctUntilChanged将跳过状态的发出,直到所选状态片段拥有新的值/对象引用。this.state$.asObservable()确保该select方法返回一个 Observable(而不是AnonymousSubject)。
  • setState接受一个部分类型。这允许我们偷懒,只传递更大状态接口的部分属性。在state$.next方法内部,部分状态会与完整状态对象合并。最终,BehaviorSubjectthis.state$会发出一个全新的状态对象。

用法

必须管理某些状态的 Angular 服务可以简单地扩展 StateService 来选择和更新状态。

世界上只有一件事需要管理:TODOS!:) 让我们创建一个 TodosStateService。

interface TodoState {
  todos: Todo[];
  selectedTodoId: number;
}

const initialState: TodoState = {
  todos: [],
  selectedTodoId: undefined
};

@Injectable({
  providedIn: 'root'
})
export class TodosStateService extends StateService<TodoState>{
  todos$: Observable<Todo[]> = this.select(state => state.todos);

  selectedTodo$: Observable<Todo> = this.select((state) => {
    return state.todos.find((item) => item.id === state.selectedTodoId);
  });

  constructor() {
    super(initialState);
  }

  addTodo(todo: Todo) {
    this.setState({todos: [...this.state.todos, todo]})
  }

  selectTodo(todo: Todo) {
    this.setState({ selectedTodoId: todo.id });
  }
}
Enter fullscreen mode Exit fullscreen mode

我们来看一下 TodosStateService 代码:

  • TodosStateService 扩展StateService并传递状态接口TodoState
  • 构造函数需要调用super()并传递初始状态
  • 公共的可观察对象todos$,并将selectedTodo$相应的状态数据暴露给感兴趣的消费者,如组件或其他服务
  • 公共方法addTodoselectTodo公开公共 API 来更新状态。

与组件和后端 API 的交互

让我们看看如何将 TodosStateService 与 Angular 组件和后端 API 集成:

替代文本

  • 组件调用 TodosStateService 的公共方法来更新状态
  • 对状态感兴趣的组件只需订阅 TodosStateService 公开的相应公共 Observable。
  • API 调用与状态密切相关。API 响应通常会直接更新状态。因此,API 调用由 TodosStateService 触发。API 调用完成后,可以使用以下方式立即更新状态: setState

演示

查看使用 TodosStateService 的完整 TODOs 应用程序:
Stackblitz - Angular State Manager

笔记

不可变数据

为了在组件中受益,ChangeDetectionStrategy.OnPush我们必须确保不改变状态。
我们有责任始终将新对象传递给setState方法。如果我们要更新包含对象/数组的嵌套属性,那么我们也必须分配一个新的对象/数组。

有关不可变状态更新的更多示例,请参阅完整的TodosStateService(在 Stackblitz 上) 。

仅供参考,
有一些库可以帮助您保持状态数据不变:
Immer
ImmutableJS

具有双向数据绑定的模板驱动表单

关于不可变数据……在将状态推送到表单输入使用 的模板驱动表单时,我们必须小心[(ngModel)]。当用户更改表单输入值时,状态对象将直接变异……
但我们希望保持不变,并且仅使用 显式更改状态setState。因此,使用响应式表单是更好的选择。如果必须使用模板驱动表单,那么仍然有一个很好的折衷方案:单向数据绑定[ngModel]。另一个选择是(深度)克隆表单数据……在这种情况下,您仍然可以使用[(ngModel)]

async订阅管道

大多数情况下,组件应该使用模板中的管道订阅“State”Observable  async 。异步管道会为我们订阅,并在组件销毁时自动处理取消订阅。

异步管道还有一个好处:
当组件使用 OnPush 变更检测策略时,它们只会在以下情况下自动更新其视图:

  • 如果@Input收到一个新的值/对象引用
  • 如果组件或其子组件之一触发了 DOM 事件

在某些情况下,组件既没有 DOM 事件,也没有发生更改的 @Input。如果该组件订阅的状态在组件类内部发生变化,那么 Angular 变更检测将无法知道在观察到的状态发出后,视图需要更新。

您可以使用 来修复它ChangeDetectorRef.markForCheck()。它告诉 ChangeDetector 无论如何都要检查状态变化(在当前或下一个变更检测周期中),并在必要时更新视图。

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoShellComponent {
  todos: Todo[];

  constructor(
    private todosState: TodosStateService,
    private cdr: ChangeDetectorRef
  ) {
    this.todosState.todos$.subscribe(todos => {
      this.todos = todos;
      this.cdr.markForCheck(); // Fix View not updating
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

但我们也可以使用async模板中的管道。它会调用ChangeDetectorRef.markForCheck我们。请参阅 Angular 源代码:async_pipe

更短更漂亮:

<todo-list [todos]="todos$ | async"></todo-list>
Enter fullscreen mode Exit fullscreen mode

异步管道功能强大,订阅、取消订阅、markForCheck 等功能应有尽有。让我们尽可能地利用它。

在演示中查看异步管道的实际运行:todo-shell.component.html

select回调经常被调用

我们应该意识到,传递给该select方法的回调函数需要在每次调用时执行setState
因此,select 回调函数不应该包含繁重的计算。

多播已消失

如果该方法返回的 Observable 有多个订阅者,select那么我们会看到一些有趣的现象:BehaviorSubject 的多播功能消失了……select当状态发生变化时,传递给该方法的回调函数会被多次调用。Observable 会针对每个订阅者执行。
这是因为我们使用 将 BehaviorSubject 转换为 Observable。Observablethis.state$.asObservable()不支持多播。

幸运的是,RxJS 提供了一个(多播)操作符来使 Observable 进行多播:shareReplay

我建议只在需要的地方使用 shareReplay 操作符。假设 Observable 有多个订阅者todos$,那么我们可以像这样进行多播:

todos$: Observable<Todo[]> = this.select(state => state.todos).pipe(
    shareReplay({refCount: true, bufferSize: 1})
);
Enter fullscreen mode Exit fullscreen mode

使用它refCount: true可以避免内存泄漏,这一点很重要。bufferSize: 1它将确保延迟订阅者仍然能够获得最后发出的值。

在此处阅读有关多播运算符的更多信息:RXJS 共享运算符的魔力及其差异

外观模式

还有一件好事。状态管理服务促进了外观模式selectsetState是受保护的函数。因此它们只能在 内部调用TodosStateService。这有助于保持组件的精简和整洁,因为它们无法直接使用setState/select方法(例如在注入的 TodosStateService 中)。状态实现细节保留在 TodosStateService 内部。
外观模式使得将 TodosStateService 重构为其他状态管理解决方案(例如 NgRx)变得很容易——如果你愿意的话 :)

谢谢

特别感谢您审阅此博客文章:

对我有所启发的文章:

MiniRx

我将自己在响应式状态管理方面的经验融入到这个非常酷的库中:MiniRx Store
你可以轻松地将本文中的 StateService 重构为 MiniRx Feature Store。MiniRx Feature Store也支持setStateselect,但你还可以获得额外的好处,例如 Redux DevTools 支持、撤销、强制不变性、效果、记忆化选择器等等。

文章来源:https://dev.to/angular/simple-yet-powerful-state-management-in-angular-with-rxjs-4f8g
PREV
使用 Jest 进行软件测试
NEXT
在 Angular 中轻松设置 TailwindCSS