回调地狱与 Promise
在使用 Ajax 的过程中,经常会遇到这种情况:我们需要在一个 Ajax 里面嵌套另一个 Ajax 调用,有时候甚至需要嵌套好几层 Ajax 调用,于是就形成了所谓的“回调地狱”:
这种代码最大的问题是可读性非常差,时间长了之后根本无法维护。
Promise 的出现主要就是为了解决这个问题,在 Promise 的场景下,我们可以这样写代码:
new Promise(function(resolve,reject){ //异步操作之后用resolve返回data }) .then(function(data){ //依赖于Promise的第一个异步操作 }) .then(function(data){ //依赖于Promise的第二个异步操作 }) .then(function(data){ //依赖于Promise的第三个异步操作 }) .catch(function(reason){ //处理异常 });
很明显,这样的代码可读性就强太多了,而且未来维护起来也很方便。
当然,Promise 的作用不止于此,如果你想更细致地研究 Promise,请看 MDN 上的这篇资料。
RxJS 与 Promise 具有相似的地方,请看以下两个代码片段:
let promise = new Promise(resolve => { setTimeout(() => { resolve('---promise timeout---'); }, 2000); }); promise.then(value => console.log(value));
let stream1$ = new Observable(observer => { let timeout = setTimeout(() => { observer.next('observable timeout'); }, 2000); return () => { clearTimeout(timeout); } }); let disposable = stream1$.subscribe(value => console.log(value));
可以看到,RxJS 和 Promise 的基本用法非常类似,除了一些关键词不同。Promise 里面用的是 then() 和 resolve(),而 RxJS 里面用的是 next() 和 subscribe()。
任何一种技术或者框架,一定要有自己的特色,如果跟别人完全一样,解决的问题也和别人一样,那存在的意义和价值就会遭到质疑。
所以,RxJS 一定有和 Promise 不一样的地方,最重要的不同点有3个,请看下图:
依次给3块代码来示范一下:
let promise = new Promise(resolve => { setTimeout(() => { resolve('---promise timeout---'); }, 2000); }); promise.then(value => console.log(value));
let stream1$ = new Observable(observer => { let timeout = setTimeout(() => { observer.next('observable timeout'); }, 2000); return () => { clearTimeout(timeout); } }); let disposable = stream1$.subscribe(value => console.log(value)); setTimeout(() => { disposable.unsubscribe(); }, 1000);
从以上代码可以看到,Promise 的创建之后,动作是无法撤回的。Observable 不一样,动作可以通过 unsbscribe() 方法中途撤回,而且 Observable 在内部做了智能的处理,如果某个主题的订阅者为0,RxJS 将不会触发动作。
let stream2$ = new Observable(observer => { let count = 0; let interval = setInterval(() => { observer.next(count++); }, 1000); return () => { clearInterval(interval); } }); stream2$.subscribe(value => console.log("Observable>"+value));
以上代码里面我们用 setInterval 每隔一秒钟触发一个新的值,源源不断,就像流水一样。
这一点 Promise 是做不到的,对于 Promise 来说,最终结果要么 resole(兑现)、要么 reject(拒绝),而且都只能触发一次。如果在同一个 Promise 对象上多次调用 resolve 方法,则会抛异常。而 Observable 不一样,它可以不断地触发下一个值,就像 next() 这个方法的名字所暗示的那样。
let stream2$ = new Observable(observer => { let count = 0; let interval = setInterval(() => { observer.next(count++); }, 1000); return () => { clearInterval(interval); } }); stream2$ .filter(val=>val%2==0) .subscribe(value => console.log("filter>"+value)); stream2$ .map(value => value * value) .subscribe(value => console.log("map>"+value));
在上述代码里面,我们用到了两个工具函数:filter 和 map。
类似这样的工具方法在 Observable 里面叫做 operator(操作符),所以有人说 Observable 就相当于异步领域的 Underscore 或者 lodash,这样的比喻是非常贴切的。这也是 Observable 比较强的地方,Promise 里面就没有提供这些工具函数。
Observable 里面提供了数百个这样的“操作符”,完整的列表和API文档请参考这里。
我也看到有一些朋友在抱怨,说 RxJS 太过复杂,操作符的数量又特别多,不知道在什么场景下面应该用什么操作符。
实际上这种担心是多余的,因为在 RxJS 里面最常用的操作符不超过10个,不常用的操作符都可以在使用的时候再去查阅文档。
RxJS 和你自己开发的系统一样,常用的功能只有其中的20%,而剩余80%的功能可能永远不会被用到。所以,RxJS 并不像很多人说的那么玄乎,你一定能学会,我相信你。
this.http .get(url,{search:params}) .map((res:Response) => { let result=res.json(); console.log(result); return result; }) .catch((error:any) => Observable.throw(error || 'Server error'));
在新版本的 Angular 里面,http 服务的返回值都是 Observable 类型的对象,所以我们可以 subscribe(订阅)这个对象。当然,Observable 所提供的各种“操作符”都可以用在这个对象上面,比如上面这个例子就用到了 map 操作符。
this.searchTextStream .debounceTime(500) .distinctUntilChanged() .subscribe(searchText => { console.log(this.searchText); this.loadData(this.searchText) });
这个例子里面最有意思的部分是 debounceTime 方法和 distinctUntilChanged 方法,这是一种“去抖动”效果。“去抖动”这个场景非常能体现 Observable 的优势所在,有一些朋友可能没遇到过这种场景,我来解释一下,以防万一。
在搜索引擎里面,我们经常会看到这样的效果:
这种东西叫做“动态搜索建议”,在用户敲击键盘的过程中,浏览器已经向后台发起了请求,返回了一些结果,目的是给用户提供一些建议。
效果看起来很简单,但是如果没有这个 debounceTime 工具函数,我们自己实现起来是非常麻烦的。这里的难点在于:用户敲击键盘的过程是源源不断的,我们并不知道用户什么时候才算输入完成。所以,如果让你自己来从零开始实现这种效果,你将会不得不使用定时器,不停地注册、取消,自己实现延时,还要对各种按键码做处理。
在 Observable 里面,处理这种情况非常简单,只要一个简单的 debounceTime 加 distinctUntilChanged 调用就可以了。