Angular Reactive Extention 之 effect 函数

335 阅读3分钟

其实是装了个逼,Angular Reactive Extention 其实就是 NgRx。之前一直嘲笑,NgRx 代表着 Angular Redux, 而不是官方的 Reative Extension。直到看到了 NgRx/Component,才开始有点信服。最近看到了, NgRx/componet-store, 特别是 effect 函数,简直感觉忽然解决了我所有的问题。

之前我们提过,Angular 虽然很早就已经引入了 RxJS,但是,有些本身就是一个 obserble 的东西却迟迟没有好的支持。比如:@input, 和 event.

如果我们把 component 看作是一个 HTML 生成函数的话,它的变化因素只有两个,input 和 event,input 我们一直没有一个很优雅的 change detect 方式,而 event, 我们不得不写一堆,onEvent 函数将逻辑支离破碎的拼装到一起,我们来看下下面的例子:

@Input() userId: string;

ngOnInit(): void {
  this.users$ = this.service.getUser(this.userId);
}

ngOnChanges(changes: SimpleChanges): void {
  if (changes.userId) {
    this.users$ = this.service.getUser(this.userId);
  }
}

我们可以看到,这种基于周期函数的方法,使得我们不得不把逻辑拆分到不同的周期函数里,大量的相同的操作需要按照周期函数调用的方式,重新思考。造成冗余。

有没有可能,把 input 变成一个 observable 呢?可以的:

private userId$ = new SubjectReplay(1);
@Input()
set userId(val: string) {
  this.userId$.next(val);
}

ngOnInit(): void {
  this.users$ = this.userId$.pipe(
    switchMap(userId => this.service.getUser(userId))
  );
}

这样写,就有效的避免了相同的逻辑,需要在不同的周期函数里重复调用的问题,有效的将相同的概念,组合到了一起。

但是还是有两个问题:

  • 我们不得不定义两个含义重复的 property,userId$, userId, 甚至发现,名字都已经不太好取。
  • 本质上我们没有办法阻止,userId$.next 在其他地方调用,造成他实现了超出原本含义的操作。

对于 @Input 函数来说,本质上,每一次的修改都是一次 set 函数的调用,有没有有效的将,set 函数,转换为 observable 的方法呢。其实说白了也很简单,因为,每一次 observable 的封装都是类似的,我们是否可以通过一个公共方法来实现,这样既减少了额外 obserble 的定义,又可以有效的避免恶意调用。将函数的每一次调用,封装成一个 observable,很容易想到闭包:

function createEffect() {
  const mySubject = new Subject();
  
  return function(val: any) {
    mySubject.next(val);
  }
}

使用起来呢,也很方便:

set userId(val: string) {
  this.updateUser(userId);
}

updateUserId = createEffect();

这样一来,userId 的每次变化都会有效的记录在 updateUserId 中对应的 observable 里面,但是聪明的你可能发现了,虽然变化被记录了,但是,我们却没有办法做相应的操作,这里就有两个思路:

  • 把 observable 暴露出来
  • 传入 callback 函数

我们先来看第二种思路:

function createEffect(callback: (obse: observable<any> => void)) {
  const mySubject = new Subject();
  callback(mySubject.asObservbale());
  
  return function(val: any) {
    mySubject.next(val);
  }
}

//.....
//.....
//.....

set userId(val: string) {
  this.updateUser(userId);
}

updateUserId = createEffect((userId$) => {
  this.userId$.pipe(
    switchMap(userId => this.service.getUser(userId)),
    tap(user => {
      // xxxxxxxx
    }),
  );
});

假设,这时候,洁面上还有一个按钮可以修改 userId, 并且 call service:

<button (click)="updateUserId(userId)"></button>

这样机遇 feature 而不是 event 或者 lifecycle event 的函数就可以最大可能的实现逻辑复用。

还有第一种思路我们还没说,我们接下来一起看一下:

function createEffect(defaultValue) {
  const mySubject = new BehaviorSubject(defaultValue);
  
  const update = function(val: any) {
    mySubject.next(val);
  }
  
  return ([
    mySubject.getValue(),
    update,
  ]);
}

const [userId, updateUserId] = createEffect('zhangsan');

不用我说,你也知道,这是啥了。。。

不管怎么样,你会发现,React,Vue, Angular 可能都在往类似的 observable 的方向上走。只是方式不同罢了。