Reactive Programming 之 RxJS 中遇到的坑

454 阅读3分钟

Promise 与 Observable

受 Promise 的影响,我们常常会将 Observable 和 Promise 做类比来理解。

observable.subscribe(function next(val) {});

promise.then(function resolve(val) {});

基本上可以理解他们的区别为,next 会被调用多次,resolve 只会被调用一次。或者 promise 可以理解成,只 emit 一次 value 的 observable。

当然,subscribe 和 then 肯定是不完全等价的。其中一个最大的区别就是,then 只是对 promise resolve 的理解做相应的处理,并不会触发 promise 的执行。也就是说,一个 promise 不管有没有 then, 都会被执行,但是 observable 在没有 subscribe 的情况下,是不是被执行的。then 更加接近 observable 中的 swicthMap.

所以我们也可以简单实现 toPromise:

 class Observable {
   ...
   ...
   toPromise() {
     return new Promise((resolve) => {
       this.take(1).subscribe(resolve);
     });
   }
 }

第一个坑: observable 永远只有被 subscribe 后才会被执行。

这里就引入第一个坑,刚开始的时候,我们会按照 promise 的方式来写 observable,比如当我们不需要处理 API 结果的时候,往往会不写 then,比如:

fetch('xxxxxxxxxxx/user', {
  method: "POST"body: "",
})

对于 promise 是没有问题的,user 能够 update,只是我们并不关心它是否成功。

但是对于 observable,我们如果以相同的方式来写:

fromFetch('xxxxxxxxxxx/user', {
  method: "POST"body: "",
})

就会发现,Nextwork 中根本就没有 API call 的记录,有上面的解释很容易就可以明白:

fromFetch('xxxxxxxxxxx/user', {
  method: "POST"body: "",
}).subscribe();

subscribe 永远不能省略。我们可以简单的把 promise 理解为,创建的时候就已经 subscribe 的observable,而且 value 只会 emit 一次。

第二个坑:每一次 subscribe 都会形成一个新的执行空间,需要注意这点,避免不必要的资源消耗。

如果我之前的 API call 是关心结果的,并且要打印出结果:

const updateUser = fetch('xxxxxxxxxxx/user', {
  method: "POST"body: "",
});

updateUser.then((val) => console.log('print api result1', val));
// xxxxxxxx
// xxxxxxxx
updateUser.then((val) => console.log('print api result2', val));

这是没有问题的,你会看到,'print api result1', 'print api result2' 各被打印了一次。

但是,如果换成 observable 呢:

const updateUser = fromFetch('xxxxxxxxxxx/user', {
  method: "POST"body: "",
});

updateUser.subscribe((val) => console.log('print api result1', val));
// xxxxxxxx
// xxxxxxxx
updateUser.subscribe((val) => console.log('print api result2', val));

你会看到,'print api result1', 'print api result2' 各被打印了一次。但是同时,你会发现,ajax call 被发送了两次!

通过之前的知识,很容易就可以理解,因为 observable 本质上是一个 function, 每次 subscribe 都会有一个新的执行空间。所以,两次 subscribe 也会互相独立,打印出自己的数据。

所以 Promise 的定义又可以更新为:创建的时候就已经 subscribe 的observable,而且 value 只会 emit 一次。并且只能被 subscribe 一次。

第三个坑,Observable 中的异常处理:

我们举个例子,页面上有一个按钮,update, 点击 按钮后 call api update user.

const saveUser$ = fromEvent(document.querySelector('.saveButton'), 'click')
  .pipe(
    switchMap(() => updateUser()),
   )
   
function updateUser() {
  return fromFetch('xxxxxxxxxxx/user', {
    method: "POST"body: "",
  });
}

表面上看起来没有问题,实际上,API call 如果没有 2xx, fetch 就会 throw exception,如上果异常没有被合理捕捉,就会往上抛出,

  event$ ---0----------0-------------0----------------
             \          \              \
              \          \              \
ajax call     	---1-|     ---1-|         --1-|
saveUser$  --------1----------1-------------1----------

假设,第二个 ajax call 抛出 error:

  event$ ---0----------0-------------0----------------
             \          \              \
              \          \              \
ajax call     	---1-|     ---x-|         --1-|
saveUser$  --------1----------x-|-----------------------

因为只要出现 error,observable 就会结束,不再接收任何新的 value。这里症状就会表现为,只要有一次 api 返回不是 2xx, 后面无论怎么点击按钮都会没有效果。

解决方法也很简单,类似 catch, rxjs 也有 catchError 这样的 operator. catchError 跟 swicthMap 一样,也是一个 high order operator。

因为 catchError 的时候当前的 stream 其实已经结束了,所以,catchError 会 return 一个新的 stream。等价于,出现 error 时,switchMap 到另一个 stream。

我们可以简单的理解为:

function catchError(observable, catchErrorFun) {
  return new Observable((observer) => {
    observable.subscribe({
      error: (err) => {
      	catchErrorFun(err).subscribe(observer);
      }
    });
  });
}

所以刚刚的例子,可以这样处理:

const saveUser$ = fromEvent(document.querySelector('.saveButton'), 'click')
  .pipe(
    switchMap(() => updateUser()).pipe(
    	catchError((err) => {
          console.log(err);
          return empty();
        })
    ),
   )

这样就避免了 throw error 导致的下游 stream 结束。

这里有个知识点之前没有讲,就是 high order operator,留着坑,以后讲吧。还有就是,ajax call 在多次 subscribe 的时候会有多次,造成资源的浪费,我们直到原因了,万一我们实在需要 subscribe 多次又改如何是好呢?这里会设计到 cold <=> hot 之间的转换,以后再慢慢讲。