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 结束。