回调地狱
首先有一个需求,如何连续根据函数的依赖关系,实现多个函数的连续调用,而且要在前置函数完成的情况下。例如 1 秒钟之后执行 fn1
;fn1
执行完毕,相隔 1 秒,执行 fn2
;fn2
执行完毕,相隔 1 秒,执行 fn3
。
我们可以利用回调函数,将后续需要执行的函数作为前置函数的回调函数参数,在前置函数执行之后执行。
// exp 1
function fn1(callback) {
setTimeout(()=>{
console.log('fn1 executed');
callback();
},1000);
}
function fn2(callback) {
setTimeout(()=>{
console.log('fn2 executed');
callback();
},1000);
}
function fn3() {
setTimeout(()=>{
console.log('fn3 executed');
},1000);
}
fn1(function() {
fn2(function() {
fn3();
});
});
// "fn1 executed"
// 1s~
// "fn2 executed"
// 1s~
// "fn3 executed"
上述代码中不断嵌入回调函数,回调函数中还有函数作为参数,结果输出没有问题。但是代码缺乏可读性和拓展性,健壮性。当其中一个函数需要修改,或者嵌套回调层数增多,将陷入常说的“回调地狱”中,我们需要一种更为符合逻辑,更优雅的异步回调的方法—— Promise。
Promise 的含义
在 MDN 文档 中,它是被这样定义的:
Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及该异步操作的结果值。
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的 Promise 对象
阮一峰老师的 理解:
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise 通过对构造函数的设计,让异步编程比传统的方式更合理,处理更为逻辑化,代码也更加优雅。
语法
Promise 对象具有两个特点:1)Promise 对象分别有三种状态 pending
,fullfiled
,rejected
。最开始时是pending
,当内部异步函数执行成功,则状态立即变为fullfilled
,且不可更改;当内部异步函数执行失败,则状态立即变为rejected,且不可更改。2)Promise 对象定义后会立即执行,而 resolve
函数要等待响应的结果。
// exp 2
const promiseExp = new Promise(function(resolve, rejected) {
if(/*success condition */) {
resolve(value);
}else {
reject(error);
}
});
function ifSuccess() {
// do somthing if success
}
function ifFailure(error) {
// do somthing if fail
}
promiseExp().then(ifSuccess).catch(ifFailure(error));
上述代码中,当 resolve
或者 reject
被执行,Promise 状态从pending
(进行中) 变成fullfilled
(完成)或者 rejected
(失败);当其中任意一个发生时,就会调用相对应的函数,成功后该执行的函数或失败后该执行的函数,且由于.then()
和 .catch
方法依旧返回一个 Promise 对象,::因此可以链式调用,类似于解决文章开头的“回调地狱”的问题。
Promise 改造:
// exp 3
function fn1() {
return new Promise(function(resolve, reject) {
console.log('fn1 promise immediatly');
setTimeout(()=>{
console.log('fn1 async');
resolve();
},1000);
});
}
function fn2() {
return new Promise(function(resolve, reject) {
console.log('fn2 promise immediatly');
setTimeout(()=>{
console.log('fn2 async');
resolve();
},1000);
});
}
function fn3() {
return new Promise(function(resolve, reject) {
console.log('fn3 promise immediatly');
setTimeout(()=>{
console.log('fn3 async');
resolve();
},1000);
});
}
function onerror() {
console.log('error');
}
fn1().then(fn2).then(fn3).catch(onerror);
console.log('outer after calling');
/*
"fn1 promise immediatly"
"outer after calling"
1s~
"fn1 async"
"fn2 promise immediatly"
1s~
"fn2 async"
"fn3 promise immediatly"
1s~
"fn3 async"
*/
上述代码对最初的代码进行了改造,我们可以看到几点:
1)通过状态的变化,触发.then()
函数中的函数,我们避免了多层的回调函数嵌套,以同步的方式进行异步函数回调,更具有可读性,合理性和健壮性。
2)Promise 对象是立即执行的,体现在1s的间距,"fn1 immediatly" 是和 “outer after calling ”一起输出的,而其他的都是上一个异步函数(fnx async )和 异步完成调用函数(fnx+1 promise immediatly)一起输出的。
3).then()
返回的仍是一个 Promise 对象,因此在连续的回调函数依赖关系中,通过对 promise.prototype.then
的连续链式调用,实现了连续的函数回调(如下图)。
Promise 的原型
Promise.prototype.then()
添加解决(fulfillment
)和拒绝(rejection
)回调到当前 Promise, 返回一个新的 Promise, 将以回调的返回值来resolve
。
then(onfulfilled, onrejected)
,then()
有两个参数,一个是 resolve
状态的回调函数,一个是 reject
状态的回调函数(可选),分别对应onfullfilled
,onrejected
;根据上面的例子,then
返回的仍是 Promise 对象,因此可以实现链式调用。
// exp 4
getIp("/getIp.php").then(
ipResult => getCity(ipResult.ip)
).then(
city => getWeather(city),
err => console.log('rejected: ' + err);
)
上面的代码中,第一个then()
指定的回调函数getCity
返回的仍是一个 Promise 对象,因此继续调用then()
,此时,如果第二个then
指定的回调函数就会等待新的返回的 Promise 对象状态的变化,如果是状态变为 resolved,则执行getWeather
,如果是rejected,则执行console.log('rejected: '+err)
;
Promise.prototype.catch
和then()
一样,catch()
方法返回的是一个 Promise 对象,以及他拒绝的理由,他的行为和Promise.prototype.then(undefined, onRejected)
。实际上,ECMA 的 官方文档 就是这么写的:obj.catch(onRejected)等同于obj.then(undefined, onRejected)
在
catch
的使用中,他不仅可以捕获来自源 Promise 对象抛出的错误(下面第一个例子),也同时可以捕获在链式调用then
和catch
时,由then
抛出的错误(第二个例子)。
// exp 5
const promise = new Promise(function(resolve, reject) {
console.log('before throw');
throw new Error('error test');
console.log('after throw');
});
promise.catch(function(error) {
console.log(error);
});
// "before throw"
// "error test"
上面的代码中,我们定义了一个 Promise 对象,他的作用就是抛出一个错误,并将错误的内容传递出去,当 Promise 对象调用catch
捕获的时候,它可以直接捕获由 Promise 传递出的 error,并且立即执行完毕throw
,错误被捕捉之后,就不再执行之后的函数,因此在throw
之后的log
语句没有被执行出来。
// exp 6
const promise = new Promise(function(resolve,reject) {
resolve('error test');
});
promise.then(function(err){
console.log('now I throw an error');
throw new Error(err);
}).catch(function(err){
console.log(err);
});
// "now I throw an error"
// "error test"
上面的代码和前一段不同,在 Promise 对象中,并没有抛出错误。错误时在then
的回调函数中抛出的,可以看到,catch
不仅可以捕获来自第一个 Promise 的错误,由于链式调用的原因,还可以捕获then()
回调函数返回的 Promise 对象的错误。
前面说道:
Promise 对象具有两个特点:1)Promise 对象分别有三种状态
pending
,fullfiled
,rejected
。最开始时是pending
,当内部异步函数执行成功,则状态立即变为fullfilled
,且不可更改;当内部异步函数执行失败,则状态立即变为rejected
,且不可更改。
如果是已经执行resolve
之后,状态变成了fullfilled
,再抛出错误,会不会被catch
捕获呢?
// exp 7
const promise = new Promise(function(resolve,reject) {
resolve('The ink is dry');
throw new Error('An error after resolve');
});
promise.then(function(msg){
console.log(msg);
}).catch(function(err){
console.log(err);
});
// "The ink is dry"
不会,因为 Promise 的状态已经从pending
变成fullfilled
,就不会改变,同理如 exp 5 的 Promise 对象中的 console.log
语句,在throw
语句之前的log
被执行了,之后的log
没有被执行,因为throw
之后,Promise 的状态已经改变了,就不会再继续执行下面的代码。
Promise.prototype.finally
finally()
方法返回一个 Promise。在 Promise 结束时,无论结果是fulfilled
或者是rejected
,都会执行指定的回调函数。这为在Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在then()
和catch()
中各写一次的情况。
// exp 8
promise()
.then(val =>{/* do something success */})
.catch(err =>{/* do something fail */})
.finally(() => {/* do something whatever*/})
上面使用的案例中,我们规定了成功该做什么是,并传入一个val
值,规定了失败该做什么时,并传入错误原因,最终,我们无论成功失败,都要完成的事情,它并没有输入的参数,也无法从它确定Promise 的状态。
阮一峰老师试着实现了了finally函数
// exp 9
Promise.prototype.finally = funtion (callback) {
let P = this.constructor;
return this.then(
val => P.resolve(callback()).then( () => val),
err => P.resolve(callback()).then( () => err)
);
}
Promise 的方法
Promise.all
.all
方法的参数是一个 Promise 对象列表,而返回的仍是一个Promise 对象,当输入的所有的 Promise 对象状态都为resolved
时,返回的 Promise 新对象才返回resolve
,当有一个出现reject
时,则新返回的 Promise 返回reject
,错误原因是第一个出现失败的 Promise 的结果。
// exp 10
var promise1 = Promise.resolve(3);
var promise2 = 42;
var promise3 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then(function(values) {
console.log(values);
});
// expected output: Array [3, 42, "foo"]
上面代码中,promise.all
等待输入的三个 Promise 均完成后,尽管一些 Promise 没有包含异步函数,但是结果其结果仍然被放进了最终返回的 Promise 中。
在作为参数列表的 Promise 对象中,如果他有自己的catch
函数,当他抛出错误时,他的错误将被自己的catch
捕获,而不会被promise.all
的catch
捕获。
// exp 11
let p1 = new Promise(function(resolve, reject) {
resolve('p1 is ok');
}).then(result => result);
let p2 = new Promise(function(resolve, reject) {
throw new Error('error test');
}).then(function(msg) {
console.log(msg);
}).catch(function(err){
console.log('p2 err captrue: '+ err);
});
let promise = Promise.all([p1,p2]);
promise.then(function(msg){
console.log('promiseAll msg: '+msg);
}).catch(function(err){
console.log('promiseAll err captrue: '+err);
});
/*
"p2 err captrue: Error: error test"
["p1 is ok", undefined]
*/
上面的代码中,p1 状态为resolved
,并将resolve
的值"p1 is ok"作为结果传入返回的回调函数中;p2 则抛出了一个错误,但是这个错误被 p2 本身的catch
函数捕捉到了,catch
函数捕捉到之后,返回一个新的 Promise,此时这个 Promise 的状态是resolved
,因此,当使用 Promise.all([p1, p2])
的时候,两者的状态都为resolved
,只是 p2 没有返回的值,因此输出中,p2 的值是"undefined",如果 p2 没有自己的catch
方法,则在Promsie.all([p1,p2])
中则会调用catch
方法。
Promsie.race
Promise.race
和Promise.all
方法输入的参数一致,都是一个参数数组,只是.race
是一旦参数数组中的某一个 Promsie 完成(resolve)
或者拒绝(reject)
,状态更改,他就会返回一个新的Promsie,状态和参数列表中的第一个发生状态改变的 Promsie 一致。
// exp 12
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve,100,'promise one 100ms');
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve,200,'promise two 200ms');
});
var promise = Promise.race([p1,p2]).then(function(result){
console.log(result);
});
// ""promise one 100ms""
上面的代码中,设置了两个 Promise 对象参数,但是设置了不同的异步完成的时间,p1 比 p2 快 100ms,因此在 p1 状态发生改变,从pending
到resolved
之后,Promise.race
立即返回新的Promise
对象,状态和 p1 一直,传递的值就是 p1 的值。
Promise.resolve
Promise.resolve
方法返回一个给定解析值的 Promise 对象,也就是将现有对象转化为 Promise 对象。传入的参数可以是一个 Promise 对象,也可以是一个 thenable
。
静态使用 resolve
方法
// exp 13
Promise.resolve('resolve exp').then(function(msg){
console.log(msg);
},function(err){
console.log('Error:'+err); // 不会执行
});
// "resolve exp"
上面代码中,resolve
方法直接返回一个新的 Promise 对象,并且处于 fullfilled
状态,携带的 value
是 "resolve exp" 因此,新的 Promise 对象直接调用.then
方法。
参数是一个thenable
对象
// exp 14
let thenable = {
then:function(resolve, reject) {
resolve('resolved before throw');
reject('after resolve');
}
};
var p = Promise.resolve(thenable);
p.then(function(msg) {
console.log(msg);
},function(err){
console.log('error: ' + err);
});
上面的代码中,resolve
输入的是一个 thenable
对象,resolve
方法会将这个对象转为 Promise 对象,然后执行thenable
对象的then
方法,执行后p
的状态将变为resolved
,因此p
的then
方法将会被执行,输出thenable
传递的msg
。
需要注意的是,立即resolve()
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
// exp 15
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
/*
"one"
"two"
"three"
*/
上面的代码中,setTimeout()
是在下一轮时间循环开始时执行,Promise
在本轮时间循环结束时执行,console.log
立即执行,因此最先输出。
Promise.reject
本方法返回一个带有拒绝原因的Promise
对象,该对象的状态自然为rejected
。
静态使用reject
方法
// exp 16
Promise.reject('reject exp').then(function(reason) {
console.log(reason)},
function(reason) {
console.log('Error: ' + reason) ;
});
//Error: reject exp
上面代码生成一个 Promise 对象的实例p,状态为rejected
,回调函数会立即执行。
与resolve
方法不同的是,reject
方法传入的参数,会作为后续方法的理由,而不是像resolve
一样,将原 thenable 传入的参数传递。
// exp 17
let thenable = {
then:function(resolve, reject) {
reject('reject exp');
}
};
var p = Promise.reject(thenable);
p.then(null,function(e){
console.log('target: ' + e);
});
// "target: [object Object] "
上面函数中,传递到p.then
中的参数不是"reject exp"
字符串,而是thenable
对象本身。
await async
await
操作符用于等待一个 Promise 对象。它只能在异步函数 async function
中使用。使用 Promise 配合 await 和 async,我们已经可以像书写同步函数那样书写异步函数。
// exp 18
function resolve2second(x) {
return new Promise(resolve => {
setTimeout(()=>{
resolve(x);
},2000);
});
};
async function fn1() {
console.log('fn1 immediatly');
var x = await resolve2second(10);
console.log(x)
}
fn1();
/*
"fn1 immediatly"
// 2s~
10
*/
上面代码中,resolve2second
是一个2秒后执行的异步函数,在async 函数fn1
中,设置了await
表达式,使得x变量赋值的操作暂停,等待Promise结果出来后,由返回的resolve值再执行对x的赋权,而fn1
函数的内的console.log
函数不受影响,随fn1
立即执行
参考阅读
- Promise 对象,阮一峰。
- Promise;await;async_function,MDN。
网友评论