在深入了解了各种状态管理模式和现成的库(NgRx, Akita…)之后,你是否会产生一种敬畏,觉得它们内部的实现高深莫测,是只有框架作者才能触及的「黑魔法」?

今天,我们就来亲手打破这个迷思。雪狼将手把手带你,仅用几十行代码,打造一个我们专属的、可复用的、通用的状态管理基类 —— 一个迷你版的「NgRx」。

这个过程,不仅会让你拥有一个属于自己的实用工具,更重要的是,它将让你彻底洞悉所有状态管理库背后的核心思想,完成从「知识消费者」到「模式创造者」的蜕变。

我们的目标:一个可复用的 Store 基类#

我们不希望每个需要管理状态的服务(UsersStore, ProductsStore…)都重复去写状态管理的样板代码。我们想要一个 Store<T> 基类,它能帮我们处理掉所有「脏活累活」。任何具体的业务 Store,只需继承它,然后专注于自己的业务逻辑即可。

这个基类需要具备:

  1. 一个封装好的、私有的状态容器。

  2. 一个可供外部订阅的、公开的只读状态视图。

  3. 一套安全、规范的内部状态更新机制。

文生图:一个清晰的类继承图。顶层是一个抽象的Store<T>基类,下面有几个具体的子类,如TodosStore, UsersStore,都通过箭头指向基类,展示了继承关系。风格:UML图、信息图表、简洁。

V1:经典的 RxJS 版本#

第一步:搭建骨架,封装 BehaviorSubject#

// rxjs-store.ts
import { Observable, BehaviorSubject } from 'rxjs';
export abstract class RxjsStore<T> {
  private readonly stateSource: BehaviorSubject<T>;
  readonly state$: Observable<T>;
  constructor(initialState: T) {
    this.stateSource = new BehaviorSubject<T>(initialState);
    this.state$ = this.stateSource.asObservable();
  }
  getState(): T {
    return this.stateSource.getValue();
  }
  protected setState(newState: Partial<T>): void {
    const currentState = this.getState();
    const nextState = { ...currentState, ...newState };
    this.stateSource.next(nextState);
  }
}

通过 private readonlyasObservable(),我们建立了一道「防火墙」,确保了外界只能「看」(订阅),不能「摸」(直接调用 .next())。

第二步:学以致用,创建 TodosStore#

// todos.store.ts
import { Injectable } from '@angular/core';
import { RxjsStore } from './rxjs-store';
// ... 定义 TodosState, Todo 接口
@Injectable({ providedIn: 'root' })
export class TodosStore extends RxjsStore<TodosState> {
  constructor() {
    super({ todos: [], filter: 'ALL' });
  }
  addTodo(text: string): void {
    const newTodo: Todo = { id: Date.now(), text, completed: false };
    this.setState({
      todos: [...this.getState().todos, newTodo],
    });
  }
  // ... 其他方法
}

TodosStore 只需专注于自己的业务逻辑,所有通用的状态管理机制,都已经被 RxjsStore 基类完美封装了。

V2:更现代的 Signal 版本#

在 Signals 时代,我们也可以打造一个 Signal-based 的 Store 基类,它更简单、更现代。

// signal-store.ts
import { signal, computed, Signal, WritableSignal } from '@angular/core';
export abstract class SignalStore<T> {
  private readonly stateSource: WritableSignal<T>;
  readonly state: Signal<T>;
  constructor(initialState: T) {
    this.stateSource = signal<T>(initialState);
    this.state = this.stateSource.asReadonly();
  }
  protected patchState(newState: Partial<T>): void {
    this.stateSource.update(currentState => ({ ...currentState, ...newState }));
  }
}

对比 V1,我们用 signal 替换了 BehaviorSubject,用 asReadonly() 替换了 asObservable(),用 updatepatchState 替换了 setState。理念相通,但 API 更为简洁。

使用它来创建 TodosSignalStore

// todos-signal.store.ts
@Injectable({ providedIn: 'root' })
export class TodosSignalStore extends SignalStore<TodosState> {
  constructor() {
    super({ todos: [], filter: 'ALL' });
  }
  // --- Selectors 现在是 computed signals ---
  readonly filteredTodos = computed(() => {
    const s = this.state(); // 获取当前状态
    const { todos, filter } = s;
    if (filter === 'ALL') return todos;
    return filter === 'ACTIVE'
      ? todos.filter(t => !t.completed)
      : todos.filter(t => t.completed);
  });
  // --- Actions ---
  addTodo(text: string): void {
    const newTodo: Todo = { id: Date.now(), text, completed: false };
    this.patchState({ todos: [...this.state().todos, newTodo] });
  }
  // ...
}

Selector 自然地变成了 computed 信号,实现了高效的、自动的缓存和派生。

结语#

恭喜你!你刚刚亲手打造了两个版本的、功能完备、可复用的状态管理解决方案。它们都包含了「单一数据源」、「不可变更新」、「Action」、「Selector」等所有现代状态管理的核心思想。

这个过程告诉我们,那些看似高大上的库,其底层原理也是由这些朴素的基础构建而成的。通过亲手实现,我们不仅收获了一个工具,更收获了一份洞察力。从现在起,你已经不再是一个单纯的「库的使用者」,你已经拥有了「模式的创造者」的视野与能力。

正如古人所言:「透过现象看本质。」 —— 意指通过观察事物的表象,从而洞察其内在的、根本的规律。我们通过亲手实现状态管理基类,正是践行了这一智慧,从「使用」的表象深入到「设计」的本质,真正掌握了核心精髓。