You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CRUD操作后如何更新Async Pipe的数据/状态?求优化方案

问题

我有一个从API获取用户数据的Observable users$,还有一个在CRUD操作时通过scan操作符封装状态的Observable usersWithAction$。目前用merge操作符实现的方案能正常运行,但感觉不够优雅,想找更合理的实现方式,恳请提供建议。


现有代码

data.service.ts

interface User {
  id: number;
  name: string;
  phone: string;
}

type ActionDelete = { action: 'delete'; id: number };
type ActionAdd = { action: 'add'; user: User };

@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(private http: HttpClient) {}

  private actionSubject = new BehaviorSubject<'' | ActionAdd | ActionDelete>(
    ''
  );
  action$ = this.actionSubject.asObservable();

  users$ = this.http
    .get<User[]>('https://jsonplaceholder.typicode.com/users')
    .pipe(shareReplay(1));

  usersWithAction$ = merge(this.action$, this.users$).pipe(
    scan((acc, value) => {
      if (Array.isArray(value)) return [...value];
      else {
        if (value && value.action === 'delete') {
          return acc.filter((user) => user.id !== value.id);
        }
        if (value && value.action === 'add') {
          return [...acc, value.user];
        }
      }
    }, []),
    tap((data) => console.log(data, 'data')),
    shareReplay(1)
  );

  deleteUser(id: number) {
    // 执行相关API请求
    this.actionSubject.next({ action: 'delete', id: id });
  }

  addUser() {
    // 执行相关API请求
    this.actionSubject.next({
      action: 'add',
      user: { id: 123, name: 'John Doe', phone: '234234' },
    });
  }
}

app.component.ts

export class AppComponent {
  name = 'Angular ' + VERSION.major;
  constructor(private dataService: DataService) {}

  users$ = this.dataService.usersWithAction$;

  deleteUser(id: number) {
    this.dataService.deleteUser(id);
  }

  addUser() {
    this.dataService.addUser()
  }
}

app.component.html

<div *ngFor="let user of users$ | async" class="user-container">
  <p>{{ user.name }}</p>
  <p>{{ user.phone }}</p>
  <button type="button" (click)="deleteUser(user.id)">Delete</button>
</div>
<button type="button" (click)="addUser()">Add</button>

优化建议

1. 统一Action类型,消除无效初始值

原代码中actionSubject的初始值是空字符串,属于无效Action,建议统一Action类型并使用合法的初始标识,让类型定义更严谨:

type Action = 
  | { action: 'init' }
  | { action: 'add'; user: User }
  | { action: 'delete'; id: number };

private actionSubject = new BehaviorSubject<Action>({ action: 'init' });

2. 改用concat保证初始数据优先加载

merge会让多个Observable的事件并行触发,存在Action先于初始用户数据发出的风险(比如API请求延迟时),改用concat可以确保先获取初始用户数据,再处理后续操作:

usersWithAction$ = concat(this.users$, this.action$).pipe(
  scan((acc, value) => {
    if (Array.isArray(value)) {
      return [...value];
    } else {
      switch(value.action) {
        case 'add':
          return [...acc, value.user];
        case 'delete':
          return acc.filter(user => user.id !== value.id);
        default:
          return acc;
      }
    }
  }, []),
  shareReplay(1)
);

3. 拆分状态处理逻辑,提升可读性

把每个Action对应的状态更新逻辑抽成独立函数,让scan内部逻辑更简洁、易维护:

private handleAdd(acc: User[], user: User): User[] {
  return [...acc, user];
}

private handleDelete(acc: User[], id: number): User[] {
  return acc.filter(user => user.id !== id);
}

usersWithAction$ = concat(this.users$, this.action$).pipe(
  scan((acc, value) => {
    if (Array.isArray(value)) return [...value];
    
    switch(value.action) {
      case 'add':
        return this.handleAdd(acc, value.user);
      case 'delete':
        return this.handleDelete(acc, value.id);
      default:
        return acc;
    }
  }, []),
  shareReplay(1)
);

4. 直接将初始数据作为scan的初始状态(可选)

如果能确保初始用户数据可提前获取,也可以将users$的结果作为scan的初始值,配合switchMap实现更直观的状态流转:

usersWithAction$ = this.users$.pipe(
  switchMap(initialUsers => this.action$.pipe(
    scan((acc, action) => {
      switch(action.action) {
        case 'add':
          return [...acc, action.user];
        case 'delete':
          return acc.filter(user => user.id !== action.id);
        default:
          return acc;
      }
    }, initialUsers)
  )),
  shareReplay(1)
);

内容的提问来源于stack exchange,提问作者Noob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 20:33:25