1.promise
在传统的解决方案中,js实现异步编程采用的方法是回调函数和事件监听(事件发布订阅),但是当应用很复杂很庞大时,大量的回调会让调试程序变得举步维艰,成为开发者的噩梦。
promise是在es6标准中的一种用于解决异步编程的解决方案,由于在语言级别上,不同于Java、Python等多线程语言,js是单线程的,所以在node.js中大量使用了异步编程的技术,这样做是为了避免同步阻塞。
promise意为承诺,拟定一个承诺,当承诺实现时即返回结果,不受其他操作的影响,可以把它理解为一个简单的容器,里面存放着一个将来会结束的事件返回结果(即异步操作)。不同于传统的回调函数,在promise中,所有的异步操作的结果都可以通过统一的方法处理。promise有三种状态:
pending(进行中),resolved(成功),rejected(失败),异步操作的结果决定了当前为哪一种状态,promise的状态只有两种改变情况,且仅改变一次:由pending转变为resolved,由pending转变为rejected,结果将会保持不变。
下面代码是一个简单的promise实例:
const promise = new Promise(function(resolve, reject){
//some code
if(/*异步操作成功*/){
resolve(value);
} else {
reject(error);
}
});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数。promise实例生成后,可以用then
方法分别指定resolved
和rejected
的回调函数。其中第二个函数是可选的,下面是一个Promise对象的简单例子:
function timeout(ms){
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, "done");
});
}
timeout(100).then((value) => {
console.log(value);
});
timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果,过了指定的时间后,Promise状态变为resolved,触发then方法绑定的回调函数。
let promise = new Promise(function(resolve, reject){
console.log("Promise");
resolve();
});
promise.then(function(){
console.log("resolved.");
});
console.log("Hi!");
// Promise
// Hi!
// resolved.
上面代码中,Promise新建后立即执行,所以首先返回的是Promise,然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
以上简单介绍了promise的基本特性:一旦创建立即执行;三种状态:执行中,成功,失败;结果不受其他操作影响,结果不可取消;当异步操作完成或失败时,Promise会处理一个单个事件。
2.Observable
Observable即可观察对象,在很多软件编程任务中,或多或少你都会期望你写的代码能按照编写的顺序,一次一个的顺序执行和完成。但是在ReactiveX(基于一系列可观察的异步和基础事件编程组成的一个库)中,很多指令可能是并行执行的,之后他们的执行结果才会被观察者捕获,顺序是不确定的。为达到这个目的,你定义一种获取和变换数据的机制,而不是调用一个方法。在这种机制下,存在一个可观察对象(Observable),观察者(Observer)订阅(Subscribe)它,当数据就绪时,之前定义的机制就会分发数据给一直处于等待状态的观察者哨兵。
ReactiveX 可见模式允许你使用数组等数据项的集合来进行些异步事件流组合操作。它使你从繁琐的web式回调中解脱,从而能使得代码可读性大大提高,同时减少bug的产生。
- observable数据是很灵活的,不同于promise只能处理单个值,observalbe支持多值甚至是数据流。
- 当observalbes被创建时,它是不会立即执行的(lazy evaluation),只有当真正需要结果的时候才会去调用它。例如下面的代码,对于promise而言,无论是否调用then,promise都会被执行;而observables却只是被创建,并不会执行,而只有在真正需要结果的时候,如这里的foreach,才会被执行。
var promise = new Promise((resolve) => {
setTimeout(() => {
resolve(42);
}, 500);
console.log("promise started");
});
promise.then((x) => console.log(x));
var source = Rx.Observable.create((observe) => {
setTimeout(() => {
observe.onNext(42);
}, 500);
console.log("observable started");
});
source.forEach((x) => console.log(x));
- observables是可以取消的(dispose),observables能够在执行前或执行中被取消,即取消订阅。下面的例子中,observable在0.5秒的时候被dispose,所以日志“observable timeout hit”不会被打印。
var promise = new Promise((resolve) => {
setTimeout(() => {
console.log("promise timeout hit");
resolve(42);
}, 1000);
console.log("promise started");
});
promise.then((x) => console.log(x));
var source = Rx.Observable.create((observe) => {
id = setTimeout(() => {
console.log("observable timeout hit");
observe.onNext(42);
}, 1000);
console.log("observable started");
return () => {
console.log("dispose called");
clearTimeout(id);
};
});
var disposable = source.forEach((x) => console.log(x));
setTimeout(() => {
disposable.dispose();
}, 500);
- observables可以多次调用(retry),对于一个observable对象返回的结果,可以被多次调用处理,能够触发多次异步操作,在observables中封装了很多工具方法可以用来操作observable结果,对其进行组合变换。在上面的代码中,可以拿到promise和observable的变量。对于promise,不论在后面怎么调用then,实际上的异步操作只会被执行一次,多次调用没有效果;但是对于observable,多次调用forEach或者使用retry方法,能够触发多次异步操作。
下面再通过一个angular2实例场景了解promise和observable在实际应用中的区别:
首先,我们来定义一下问题的场景。假设我们要实现一个搜索功能,有一个简单的输入框,当用户输入文字的时候,实时的利用输入的文字进行查询,并显示查询的结果。
在这个简单的场景当中,一般需要考虑3个问题:
- 不能在用户输入每个字符的时候就触发搜索。
如果用户输入每个字符就触发搜索,一来浪费服务器资源,二来客户端频繁触发搜索,以及更新搜索结果,也会影响客户端的响应。一般这个问题,都是通过加一些延时来避免。 - 如果用户输入的文本没有变化,就不应该重新搜索。
假设用户输入了’foo’以后,停顿了一会,触发了搜索,再敲了一个字符’o’,结果发现打错了,又删掉了这个字符。如果这个时候用户又停顿一会,导致触发了搜索,这次的文本’foo’跟之前搜索的时候的文本是一样的,所以不应该再次搜索。 - 要考虑服务器的异步返回的问题。
当我们使用异步的方式往服务器端发送多个请求的时候,我们需要注意接受返回的顺序是无法保证的。比如我们先后搜索了2个单词’computer’, ‘car’, 虽然’car’这个词是后来搜的,但是有可能服务器处理这个搜索比较快,就先返回结果。这样页面就会先显示’car’的搜索结果,然后等收到’computer’的搜索结果的时候,再显示’computer’的结果。但是,这时候在用户看来明明搜索的是’car’,却显示的是另外的结果。
首先是promise的初始版本:
import { Injectable } from '@angular/core';
import { URLSearchParams, Jsonp } from '@angular/http';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
var search = new URLSearchParams()
search.set('action', 'opensearch');
search.set('search', term);
search.set('format', 'json');
return this.jsonp
.get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })
.toPromise()
.then((response) => response.json()[1]);
}
}
在上面代码中,使用Jsonp模块来请求api结果,它的结果应该是一个类型为Observable
下面应用observable实现功能:
(1)控制用户输入延时
export class AppComponent {
items: Array;
term = new FormControl();
constructor(private wikipediaService: WikipediaService) {
this.term.valueChanges
.debounceTime(400)
.subscribe(term => this.wikipediaService.search(term)
.then(items => this.items = items));
}
}
这里的this.term.valueChanges是一个Observable
subscribe((term) =>
this.wikipediaService.search(term).then((items) => (this.items = items))
);
这样就解决了第一个问题,通过控制用户输入延时来解决每次输入一个字符就触发一次搜索的问题。
(2)防止触发两次
this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe((term) =>
this.wikipediaService.search(term).then((items) => (this.items = items))
);
上面的代码解决了第二个问题,就是经过400ms的延时以后,用户输入的搜索条件一样的情况。Observable有一个distinctUntilChanged的方法,他会判断从消息源过来的新数据跟上次的数据是否一致,只有不一致才会触发订阅的方法。
(3)处理返回顺序
上面描述了服务器端异步返回数据的时候,返回顺序不一致出现的问题。对于这个问题,我们的解决办法就比较直接,也就是对于之前的请求返回的结果,直接忽略,只处理在页面上用户最后一次发起的请求的结果。我们可以利用Observable
的dispose()
方法来解决。实际上,我们是利用这种’disposable’特性来解决,而不是直接调用dispose()
方法。(实在不知道该怎么翻译’disposable’,它的意思是我可以中止在Observable对象上的消息处理,字面的意思是可被丢弃的、一次性的。)
search (term: string) {
var search = new URLSearchParams()
search.set('action', 'opensearch');
search.set('search', term);
search.set('format', 'json');
return this.jsonp
.get('http://en.wikipedia.org/w/api.php?callback=JSONP_CALLBACK', { search })
.map((response) => response.json()[1]);
}
注意这个方法最后用.map((response) => response.json()[1]),意思是对于原先的Response类型的结果,转换成实际的搜索结果的列表,即利用Observable的特性去丢弃上一个未及时返回的结果。
总结:在处理某些复杂异步应用中,observable比promise更受开发者青睐,因为使用Observable创建的异步任务,可以被处理,而且是延时加载的。而promise设计的初衷只是为了解决大量的异步回调所造成的难以调试问题,observable封装了大量的方法供我们使用以处理复杂的异步任务。
以上内容部分摘自阮一峰阮老师的《es6入门》,关于promise和observable的区别,还有一个较好的视频:egghead.io上的这个7分钟的视频(作者Ben Lesh)。