Promise使用详解(转)

2015年6月, ES2015(即 ECMAScript 6、ES6) 正式发布。其中 Promise 被列为正式规范,成为 ES6 中最重要的特性之一。
Promise对象有以下两个特点:
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

缺点:
首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

1.then()方法

简单来讲,then 方法就是把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。
而 Promise 的优势就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。

(1)下面通过样例作为演示,我们定义做饭、吃饭、洗碗(cook、eat、wash)这三个方法,它们是层层依赖的关系,下一步的的操作需要使用上一部操作的结果。(这里使用 setTimeout 模拟异步操作)

//做饭
function cook(){
	console.log('开始做饭。');
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('做饭完毕!');
			resolve('鸡蛋炒饭');
		}, 1000);
	});
	return p;
}

//吃饭
function eat(data){
	console.log('开始吃饭:' + data);
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('吃饭完毕!');
			resolve('一块碗和一双筷子');
		}, 2000);
	});
	return p;
}

function wash(data){
	console.log('开始洗碗:' + data);
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('洗碗完毕!');
			resolve('干净的碗筷');
		}, 2000);
	});
	return p;
}

(2)使用 then 链式调用这三个方法:

cook()
.then(function(data){
return eat(data);
})
.then(function(data){
return wash(data);
})
.then(function(data){
console.log(data);
});

当然上面代码还可以简化成如下:

cook()
.then(eat)
.then(wash)
.then(function(data){
	console.log(data);
});

(3)运行结果如下:
Promise使用详解(转)_第1张图片

2.reject()方法

上面样例我们通过 resolve 方法把 Promise 的状态置为完成态(Resolved),这时 then 方法就能捕捉到变化,并执行“成功”情况的回调。
而 reject 方法就是把 Promise 的状态置为已失败(Rejected),这时 then 方法执行“失败”情况的回调(then 方法的第二参数)。

(1)下面同样使用一个样例做演示

//做饭
function cook(){
	console.log('开始做饭。');
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('做饭失败!');
			reject('烧焦的米饭');
		}, 1000);
	});
	return p;
}

//吃饭
function eat(data){
	console.log('开始吃饭:' + data);
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('吃饭完毕!');
			resolve('一块碗和一双筷子');
		}, 2000);
	});
	return p;
}

cook()
.then(eat, function(data){
	console.log(data + '没法吃!');
})

运行结果如下:
Promise使用详解(转)_第2张图片
(2)如果我们只要处理失败的情况可以使用 then(null, …),或是使用接下来要讲的 catch 方法。

cook()
.then(null, function(data){
	console.log(data + '没法吃!');
})

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样:

const p1 = new Promise(function (resolve, reject) {
	setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
	setTimeout(() => resolve(p1), 1000)
})

p2
	.then(result => console.log(result))
	.catch(error => console.log(error))
// Error: fail	

上面代码中,p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

3.catch()方法

(1)它可以和 then 的第二个参数一样,用来指定 reject 的回调

cook()
.then(eat)
.catch(function(data){
	console.log(data + '没法吃!');
});

(2)它的另一个作用是,当执行 resolve 的回调(也就是上面 then 中的第一个参数)时,如果抛出异常了(代码出错了),那么也不会报错卡死 js,而是会进到这个 catch 方法中。

//做饭
function cook(){
	console.log('开始做饭。');
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('做饭完毕!');
			resolve('鸡蛋炒饭');
		}, 1000);
	});
	return p;
}

//吃饭
function eat(data){
	console.log('开始吃饭:' + data);
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('吃饭完毕!');
			resolve('一块碗和一双筷子');
		}, 2000);
	});
	return p;
}

cook()
.then(function(data){
	throw new Error('米饭被打翻了!');
	eat(data);
})
.catch(function(data){
	console.log(data);
});

运行结果如下:
Promise使用详解(转)_第3张图片

这种错误的捕获是非常有用的,因为它能够帮助我们在开发中识别代码错误。比如,在一个 then() 方法内部的任意地方,我们做了一个 JSON.parse() 操作,如果 JSON 参数不合法那么它就会抛出一个同步错误。用回调的话该错误就会被吞噬掉,但是用 promises 我们可以轻松的在 catch() 方法里处理掉该错误。

(3)还可以添加多个 catch,实现更加精准的异常捕获。

somePromise.then(function() {
	return a();
}).catch(TypeError, function(e) {
	//If a is defined, will end up here because
	//it is a type error to reference property of undefined
}).catch(ReferenceError, function(e) {
	//Will end up here if a wasn't defined at all
}).catch(function(e) {
	//Generic catch-the rest, error wasn't TypeError nor
	//ReferenceErro;
});

4.all()方法

Promise 的 all 方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。

(1)比如下面代码,两个个异步操作是并行执行的,等到它们都执行完后才会进到 then 里面。同时 all 会把所有异步操作的结果放进一个数组中传给 then。

//切菜
function cutUp(){
	console.log('开始切菜。');
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('切菜完毕!');
			resolve('切好的菜');
		}, 1000);
	});
	return p;
}

//烧水
function boil(){
	console.log('开始烧水。');
	var p = new Promise(function(resolve, reject){        //做一些异步操作
		setTimeout(function(){
			console.log('烧水完毕!');
			resolve('烧好的水');
		}, 1000);
	});
	return p;
}

Promise
.all([cutUp(), boil()])
.then(function(results){
	console.log("准备工作完毕:");
	console.log(results);
});

(2)运行结果如下:
Promise使用详解(转)_第4张图片

5.race()方法

race 按字面解释,就是赛跑的意思。race 的用法与 all 一样,只不过 all 是等所有异步操作都执行完毕后才执行 then 回调。而 race 的话只要有一个异步操作执行完毕,就立刻执行 then 回调。
注意:其它没有执行完毕的异步操作仍然会继续执行,而不是停止。

(1)这里我们将上面样例的 all 改成 race

Promise
.race([cutUp(), boil()])
.then(function(results){
	console.log("准备工作完毕:");
	console.log(results);
});

Promise使用详解(转)_第5张图片
(2)race 使用场景很多。比如我们可以用 race 给某个异步请求设置超时时间,并且在超时后执行相应的操作。

//请求某个图片资源
function requestImg(){
	var p = new Promise(function(resolve, reject){
		var img = new Image();
		img.onload = function(){
			resolve(img);
		}
		img.src = 'xxxxxx';
	});
	return p;
}

//延时函数,用于给请求计时
function timeout(){
	var p = new Promise(function(resolve, reject){
		setTimeout(function(){
			reject('图片请求超时');
		}, 5000);
	});
	return p;
}

Promise
.race([requestImg(), timeout()])
.then(function(results){
	console.log(results);
})
.catch(function(reason){
	console.log(reason);
});

上面代码 requestImg 函数异步请求一张图片,timeout 函数是一个延时 5 秒的异步操作。我们将它们一起放在 race 中赛跑。

  • 如果 5 秒内图片请求成功那么便进入 then 方法,执行正常的流程。
  • 如果 5 秒钟图片还未成功返回,那么则进入
    catch,报“图片请求超时”的信息。

在这里插入图片描述

6.finally()方法

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
	.then(result => {···})
	.catch(error => {···})
	.finally(() => {···});

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

7.resolve()方法

将现有对象转为 Promise 对象。

Promise.resolve方法的参数分成四种情况:

(1)参数是一个 Promise 实例

如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

(2)参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
	then: function(resolve, reject) {
		resolve(42);
	}
};

Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

let thenable = {
	then: function(resolve, reject) {
		resolve(42);
	}
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
	console.log(value);  // 42
});

上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。

(3)参数不是具有then方法的对象,或根本就不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。

const p = Promise.resolve('Hello');

p.then(function (s){
	console.log(s)
});
// Hello

上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。

(4)不带有任何参数

Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法。

const p = Promise.resolve();

p.then(function () {
	// ...
});

上面代码的变量p就是一个 Promise 对象。

需要注意的是,立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

setTimeout(function () {
	console.log('three');
}, 0);

Promise.resolve().then(function () {
	console.log('two');
});

console.log('one');

// one
// two
// three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log(‘one’)则是立即执行,因此最先输出。

8.reject()方法

返回一个新的 Promise 实例,该实例的状态为rejected。

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
	console.log(s)
});
// 出错了

9.try()方法

实现同步函数同步操作,异步函数异步操作。

(1)async函数

const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next

上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。

(async () => f())()
.then(...)

需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。

(async () => f())()
.then(...)
.catch(...)

(2)new Promise()

const f = () => console.log('now');
(
	() => new Promise(
		resolve => resolve(f())
	)
)();
console.log('next');
// now
// next

上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。

(3)try方法

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

可以统一用promise.catch()捕获所有同步和异步的错误。

Promise.try(() => database.users.get({id: userId}))
	.then(...)
	.catch(...)

事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

你可能感兴趣的:(前端手记,Promise)