不要遵循 RxJS 最佳实践 不要取消订阅 在组件内部使用订阅 在组件内部使用订阅 在组件内部使用订阅… 永远不要使用纯函数 始终手动订阅,不要使用异步 从你的服务中暴露主题 始终将流传递给子组件 弹珠图?不,这不适合你

2025-05-25

不要遵循 RxJS 最佳实践

不要取消订阅

使用内部订阅内部订阅内部订阅...

永远不要使用纯函数

始终手动订阅,不要使用异步

展示您服务中的主题

始终将流传递给子组件

大理石图?不,它不适合你

如今,越来越多的开发者学习 RxJS,并根据最佳实践正确使用它。但我们不应该这样做。所有那些所谓的最佳实践都需要学习新知识,并在项目中添加额外的代码。
此外,使用最佳实践,我们可能会失去创建良好代码库并让团队成员满意的机会!🌈
别再做一堆灰色的东西了!打破常规!别再用最佳实践了!

以下是我对如何处理 Angular 中所谓的 RxJS 最佳实践的建议:


不要取消订阅

每个人都说我们必须始终取消订阅可观察对象以防止内存泄漏。

但我并不认同。说真的,谁规定你必须取消订阅可观察对象?你完全没必要这么做。我们来玩个游戏吧!看看这些 Angular 组件的取消订阅实现哪个最好?

有操作员的那个takeUntil吗?



@Component({ ... })
export class MyComponent implements OnInit, OnDestroy {

  private destroyed$ = new Subject();

  ngOnInit() {
    myInfiniteStream$
      .pipe(takeUntil(this.destroyed$))
      .subscribe(() => ...);
  }

  ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}


Enter fullscreen mode Exit fullscreen mode

还是带操作员的那个takeWhile



@Component({ ... })
export class MyComponent implements OnInit, OnDestroy {
  private alive = true;
  ngOnInit() {
    myInfiniteStream$
      .pipe(takeWhile(() => this.alive))
      .subscribe(() => ...);
  }
  ngOnDestroy() {
    this.alive = false;
  }
}


Enter fullscreen mode Exit fullscreen mode

完全正确!都不是!两个takeWhileandtakeUntil运算符都是隐式的,可能难以阅读🤓(讽刺)。最好的解决方案是将每个订阅存储在单独的变量中,然后在组件销毁时以显式的方式取消订阅:



@Component({ ... })
export class MyComponent implements OnInit, OnDestroy {

  private subscription;

  ngOnInit() {
    this.subscription = myInfiniteStream$
      .subscribe(() => ...);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}


Enter fullscreen mode Exit fullscreen mode

当您有多个订阅时,这种方法尤其有效:



Component({ ... })
export class MyComponent implements OnInit, OnDestroy {

  private subscription1;
  private subscription2;
  private subscription3;
  private subscription4;
  private subscription5;

  ngOnInit() {
    this.subscription1 = myInfiniteStream1$
      .subscribe(() => ...);
        this.subscription2 = myInfiniteStream2$
      .subscribe(() => ...);
        this.subscription3 = myInfiniteStream3$
      .subscribe(() => ...);
        this.subscription4 = myInfiniteStream4$
      .subscribe(() => ...);
        this.subscription5 = myInfiniteStream5$
      .subscribe(() => ...);
  }

  ngOnDestroy() {
    this.subscription1.unsubscribe();
    this.subscription2.unsubscribe();
    this.subscription3.unsubscribe();
    this.subscription4.unsubscribe();
    this.subscription5.unsubscribe(); 
  }
}


Enter fullscreen mode Exit fullscreen mode

但这个解决方案还不够完美。还有什么可以改进的呢?你觉得怎么样?如何才能让代码更简洁易读?
没错,我有答案!让我们彻底删除所有丑陋的取消订阅语句。



@Component({ ... })
export class MyComponent implements OnInit {

  ngOnInit() {
    myInfiniteStream$
      .subscribe(() => ...);
  }
}


Enter fullscreen mode Exit fullscreen mode

太棒了!我们删除了所有冗余代码,现在看起来更简洁了,甚至还节省了一点硬盘空间。但是myInfiniteStream$订阅制会怎么样呢?

算了吧!😅 还是把这个工作留给垃圾收集器吧,不然它还有什么存在的意义呢?


使用内部订阅内部订阅内部订阅...

每个人都说我们应该使用*Map操作符来链接可观察对象而不是在订阅内部进行订阅,以防止回调地狱。

但我实在不同意。说真的,为什么不呢?我们为什么要用那么多switchMap/mergeMap操作符?你觉得这段代码怎么样?易读吗?你真的那么喜欢你的队友吗?



getUser().pipe(
  switchMap(user => getDetails(user)),
  switchMap(details => getPosts(details)),
  switchMap(posts => getComments(posts)),
)


Enter fullscreen mode Exit fullscreen mode

你不觉得这太简洁可爱了吗?你不应该这样写代码!你还有另一个选择,看看这里:



getUser().subscribe(user => {
  getDetails(user).subscribe(details => {
    getPosts(details).subscribe(posts => {
      getComments(posts).subscribe(comments => {  

        // handle all the data here
      });
    });
  });
})


Enter fullscreen mode Exit fullscreen mode

好多了,不是吗?!如果你讨厌你的队友,又不想学习新的 RxJS 操作符,那就一直这样写代码吧。

明亮起来!让你的团队成员通过回调地狱感受到一丝怀旧之情。


永远不要使用纯函数

每个人都说我们应该使用纯函数来使我们的代码可预测且更易于测试。

但我不能同意。说真的,为什么要用纯函数?可测试性?可组合性?这太难了,影响全局变量要容易得多。我们来看一个例子:



function calculateTax(tax: number, productPrice: number) {
 return (productPrice * (tax / 100)) + productPrice; 
}


Enter fullscreen mode Exit fullscreen mode

例如,我们有一个计算税额的函数——它是一个纯函数,对于相同的参数,它总是返回相同的结果。它很容易测试,也很容易与其他函数组合。但是,我们真的需要这种行为吗?我不这么认为。使用没有参数的函数会更简单:



window.tax = 20;
window.productPrice = 200;

function calculateTax() {
 return (productPrice * (tax / 100)) + productPrice; 
}


Enter fullscreen mode Exit fullscreen mode

确实,还能出什么问题呢?😉


始终手动订阅,不要使用异步

每个人都说我们必须async在 Angular 模板中使用管道来促进组件中的订阅管理。

但我不能同意。我们已经和 讨论过订阅管理了,takeUntil并且takeWhile一致认为这些操作符来自邪恶势力。但是,我们为什么要用async另一种方式对待管道呢?



@Component({  
  template: `
    <span>{{ data$ | async }}</span>
  `,
})
export class MyComponent implements OnInit {

  data$: Observable<Data>;

  ngOnInit() {
    this.data$ = myInfiniteStream$;
  }
}


Enter fullscreen mode Exit fullscreen mode

你看到了吗?代码简洁、易读、易于维护!啊,这不被允许。对我来说,更好的做法是把数据放在局部变量里,然后在模板中直接使用这个变量。



@Component({  
  template: `
    <span>{{ data }}</span>
  `,
})
export class MyComponent implements OnInit {
  data;

  ngOnInit() {

    myInfiniteStream$
      .subscribe(data => this.data = data);
  }
}


Enter fullscreen mode Exit fullscreen mode

展示您服务中的主题

在 Angular 中使用可观察数据服务是一种非常常见的做法:



@Injectable({ providedIn: 'root' })
export class DataService {

  private data: BehaviorSubject = new BehaviorSubject('bar');

  readonly data$: Observable = this.data.asObservable();

  foo() {
    this.data$.next('foo');
  }

  bar() {
    this.data$.next('bar');
  }
}


Enter fullscreen mode Exit fullscreen mode

这里我们将数据流暴露为可观察对象,只是为了确保它只能通过数据服务接口进行更改。但这很容易让人感到困惑。

您想要更改数据 - 您必须更改数据。

如果我们可以更改地点的数据,为什么还要添加额外的方法呢?让我们重写服务,使其更易于使用;



@Injectable({ providedIn: 'root' })
export class DataService {
  public data$: BehaviorSubject = new BehaviorSubject('bar');
}


Enter fullscreen mode Exit fullscreen mode

耶!看到了吗?我们的数据服务变得更小巧,也更易读了!而且,现在我们几乎可以将任何东西都放进数据流里了!太棒了,你不觉得吗?🔥


始终将流传递给子组件

你听说过智能/转储组件模式吗?它可以帮助我们解耦组件。此外,该模式还可以防止子组件触发父组件中的操作:



@Component({
  selector: 'app-parent',
  template: `
    <app-child [data]="data$ | async"></app-child>
  `,
})
class ParentComponent implements OnInit {

  data$: Observable<Data>;

  ngOnInit() {
    this.data$ = this.http.get(...);
  }
}

@Component({
  selector: 'app-child',
})
class ChildComponent {
  @Input() data: Data;
}


Enter fullscreen mode Exit fullscreen mode

你喜欢吗?你的队友也喜欢。如果你想报复他们,你需要按如下方式重写你的代码:



@Component({
  selector: 'app-parent',
  template: `
    <app-child [data$]="data$"></app-child>
  `,
})
class ParentComponent {

  data$ = this.http.get(...);
  ...
}

@Component({
  selector: 'app-child',
})
class ChildComponent implements OnInit {

  @Input() data$: Observable<Data>;

  data: Data;

  ngOnInit(){
    // Trigger data fetch only here
    this.data$.subscribe(data => this.data = data);
  }
}


Enter fullscreen mode Exit fullscreen mode

你看到了吗?我们不再在父组件中处理订阅了。我们只是将订阅传递给子组件。
如果你也这么做,相信我,你的团队成员在调试时一定会哭得稀里哗啦。


大理石图?不,它不适合你

你知道弹珠图是什么吗?不知道?这对你有好处!

假设我们编写了以下函数并对其进行测试:



export function numTwoTimes(obs: Observable<number>) {
  return obs.pipe(map((x: number) => x * 2))
}


Enter fullscreen mode Exit fullscreen mode

我们很多人会用弹珠图来测试函数:



it('multiplies each number by 2', () => { 
  createScheduler().run(({ cold, expectObservable }) => {
    const values = { a: 1, b: 2, c: 3, x: 2, y: 4, z: 6 }
    const numbers$ = cold('a-b-c-|', values) as Observable<number>;
    const resultDiagram = 'x-y-z-|';
    expectObservable(numTwoTimes(numbers$)).toBe(resultDiagram, values);
  });
})


Enter fullscreen mode Exit fullscreen mode

但是,谁会想学习弹珠图这个新概念呢?谁会想写出干净简洁的代码呢?让我们用一种通用的方式重写一下测试。



it('multiplies each number by 2', done => {
  const numbers$ = interval(1000).pipe(
    take(3),
    map(n => n + 1)
  )
  // This emits: -1-2-3-|

  const numbersTwoTimes$ = numTwoTimes(numbers$)

  const results: number[] = []

  numbersTwoTimes$.subscribe(
    n => {
      results.push(n)
    },
    err => {
      done(err)
    },
    () => {
      expect(results).toEqual([ 2, 4, 6 ])
      done()
    }
  )
})


Enter fullscreen mode Exit fullscreen mode

太棒了!现在看起来好多了!


结论

如果你读完了以上所有建议,你就是英雄。但是。好吧。如果你意识到自己的想法,我有个坏消息要告诉你。这只是个玩笑。

千万别像我那篇文章里说的那样。别让你的队友哭哭啼啼、恨你。永远努力做一个正直、整洁的人。拯救世界——运用模式和最佳实践!

我只是想让你开心一点,让你的一天过得更好一点。希望你喜欢。

请继续关注并告诉我您是否有任何想要了解的特定 Angular 主题!

文章来源:https://dev.to/nikpoltoratsky/don-t-follow-rxjs-best-practices-4893
PREV
免费托管 React 应用的 10 种方法
NEXT
动态规划——解决任何 DP 面试问题的 7 个步骤 动态规划——解决任何 DP 面试问题的 7 个步骤