[译]别再遵循RxJS最佳实践了!

2,653 阅读5分钟

原文: Don't follow RxJS Best Practices

现在,有越来越多的开发者在学习 RxJS,并遵循最佳实践在使用它,但我们不应该这么做。这些所谓的最佳实践都需要学习新的东西,并在项目中添加额外的代码。此外,使用最佳实践来会增加创建一个良好的代码库且让你的同事高兴的风险!

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

  • 不要退订
  • 订阅里面接着订阅里面再接着订阅里面还是接着订阅...
  • 不要使用纯函数
  • 总是手动订阅,不要使用 async
  • 在服务中暴露subjects
  • 总是将流传递给子组件
  • 弹珠图?不,这不是给你准备的。

不要退订

每个人都认为,为了避免内存泄漏,我们总得去取消订阅。
但我不同意,讲真的,是谁决定你必须取消订阅?你没必要这么做。我们来玩个游戏吧!哪种退订方式是最好的?

那个叫 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();
  }
}

还是那个叫 takeWhile 的?

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

说实话,这俩都不是!不管是 takeWhile 还是 takeUntil 都是晦涩难懂的(🐶 狗头),最好的解决办法是将每个订阅分别存为一个变量,然后直接在组件销毁时退订:

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

  private subscription;

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

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

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

@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(); 
  }
}

但是这个解决方案不够完美,怎么做会更好呢?你觉得我们怎样才能使代码更加清晰和可读呢?
是的,答案就是——让我们移除所有的这些丑啦吧唧的取消订阅声明吧。

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

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

棒呆了!我们删除了所有的冗余代码,现在看起来更简单了,甚至为我们的硬盘节省了一些内存。但是 myInfiniteStream$ 这个 subscription 会发生什么呢?
忘了它吧! 让我们把这个工作留给垃圾回收吧,不然,它干嘛存在呢,对吗?

订阅里面接着订阅里面再接着订阅里面还是接着订阅...

每个人都说我们应该使用 *Map 操作符,而不是在订阅内订阅来防止地狱回调。
但我不同意,讲真的,为什么不呢?为什么我们要使用所有这些 switchMap / mergeMap 运算符?你觉得这个代码怎么样?容易阅读?你真的那么喜欢你的同事吗?

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

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

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

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

好多了是吧?!如果你讨厌你的队友并且不想学习新的 RxJS 操作符,那么一定要以这种方式编写代码。
开心点!让你的同事们对地狱回调有一点怀旧之情。

不要使用纯函数

每个人都说我们应该用纯函数来使我们的代码可预测和更容易测试。
但我不同意,讲真的,为什么要使用纯函数呢?可测试性?可组合性?这很难,改变世界会变得更容易。让我们看看这个例子:

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

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

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

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

事实上,这哪能出错呢?😉

总是手动订阅,不要使用 async

每个人都说我们应该在 Angular 中使用 async 管道来简化组件中的订阅管理。
但我不同意,讲真的,我们已经讨论过 takeUntiltakeWhile 的订阅管理,并且一致认为这些操作符是邪恶的。
但是,为什么我们要用另一种方式来对待 async 呢。

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

  data$: Observable<Data>;

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

你看到了吗?清洁,可读,易于维护的代码!啊,这是不允许的。
对于我来说,最好是将数据放在局部变量中,并在模板中使用该变量。

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

  ngOnInit() {

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

在服务中暴露subjects

在 Angular 中使用 Observable Data Services 有一个非常普遍的实践:

@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');
  }
}

在这里,我们将数据作转为 Observable 暴露出去了,只是为了确保它只能通过数据服务接口进行更改,但是它让人感到困惑。 你想要改变数据——你必须改变数据。 如果我们可以就地更改数据,那为什么要添加其他方法呢?让我们重写这个服务,使它更容易使用。

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

耶!你看到了吗?我们的数据服务变得更小,更容易阅读!而且,现在我们几乎可以把任何东西放到我们的数据流中!太棒了,你不这么认为吗?

总是将流传递给子组件

您是否听说过 Smart / Dump 组件模式?它可以帮助我们实现组件之间的解耦。此外,该模式还可以防止子组件触发父组件中的操作:

@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;
}

你喜欢吗?你的同事也会喜欢。不过如果你想报复他们,你得以下面的方式重写你的代码:

@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);
  }
}

你看到了吗?我们不再在父组件中处理订阅,我们直接往子组件里传递订阅。
如果你遵循这个练习,你的同事们会在调试过程中流下血泪,相信我。

弹珠图?不,这不是给你准备的。

你知道什么是弹珠图吗?不知道?这是好事!
假设我们写了下面的函数并且要测试它:

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

很多人会使用弹珠图来测试这个功能:

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);
  });
})

但是,谁想学弹珠图的新概念?谁想编写简洁明了的代码?让我们用一种通用的方式重写测试。

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()
    }
  )
})

是啊!现在看起来好了一百倍!

总结

如果你读过上面所有的建议,你是一个英雄。
但是,好吧,如果你认出了你的思路,我有一个坏消息要告诉你。

这只是个玩笑。

拜托,千万别照我在那篇文章里说的做。永远不要让你的队友哭泣和恨你。
努力做一个正派和整洁的人,拯救世界——使用模式和最佳实践!

我只是决定让你高兴起来,让你的一天过得好一点,希望你会喜欢。

文章到此就结束了,开始产生混乱的朋友们反过头再捋一下,哈哈!