概述
Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。
首先,Promise 是一个对象,也是一个构造函数。
function f1(resolve, reject) {
// 异步代码...
}
var p1 = new Promise(f1);
上面代码中,Promise构造函数接受一个回调函数f1作为参数,f1里面是异步操作的代码。然后,返回的p1就是一个 Promise 实例。
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。
var p1 = new Promise(f1);
p1.then(f2);
上面代码中,f1的异步操作执行完成,就会执行f2。
Promise 对象的状态
Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。
异步操作未完成(pending)
异步操作成功(fulfilled)
异步操作失败(rejected)
上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。
一旦状态发生变化,就凝固了,不会再有新的状态变化。
Promise 构造函数
JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise.prototype.then()
Promise 实例的then方法,用来添加回调函数。
then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
var p1 = new Promise(function (resolve, reject) {
resolve('成功');
});
p1.then(console.log, console.error);
// "成功"
var p2 = new Promise(function (resolve, reject) {
reject(new Error('失败'));
});
p2.then(console.log, console.error);
// Error: 失败
上面代码中,p1和p2都是Promise 实例,它们的then方法绑定两个回调函数:成功时的回调函数console.log,失败时的回调函数console.error(可以省略)。p1的状态变为成功,p2的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。
then方法可以链式使用。
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
上面代码中,p1后面有四个then,意味依次有四个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。
最后一个then方法,回调函数是console.log和console.error,用法上有一点重要的区别。console.log只显示step3的返回值,而console.error可以显示p1、step1、step2、step3之中任意一个发生的错误。举例来说,如果step1的状态变为rejected,那么step2和step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性。
Promise 新建后就会立即执行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
注意,调用resolve或reject并不会终结 Promise 的参数函数的执行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
then() 用法辨析
看下面四种写法,它们的差别在哪里?
// 写法一
f1().then(function () {
return f2();
});
// 写法二
f1().then(function () {
f2();
});
// 写法三
f1().then(f2());
// 写法四
f1().then(f2);
为了便于讲解,下面这四种写法都再用then方法接一个回调函数f3。
var f1 = new Promise(function (resolve, reject) {
resolve('func1成功');
});
var f2 = function(e) {
console.log('f2', e);
return 'func2';
}
var f3 = function(e) {
console.log('f3', e);
return 'func3';
}
f1.then(function fn(e) {
console.log('fn', e);
return f2();
}).then(f3);
结果:
[Log] fn – "func1成功"
[Log] f2 – undefined
[Log] f3 – "func2"
< Promise = $1
result: "func3"
status: "resolved"
“Promise”原型
结论:
fn回调函数的参数,是Promise 实例f1的运行结果; f3回调函数的参数,是f2函数的运行结果。
f1.then(function fn(e) {
console.log('fn', e);
f2();
}).then(f3);
结果:
[Log] fn – "func1成功"
[Log] f2 – undefined
[Log] f3 – undefined
< Promise = $2
result: "func3"
status: "resolved"
“Promise”原型
结论:
fn回调函数的参数,是Promise 实例f1的运行结果。
f1.then(f2())
.then(f3);
结果:
[Log] f2 – undefined
[Log] f3 – "func1成功"
< Promise = $3
result: "func3"
status: "resolved"
“Promise”原型
结论:
第一个then方法的参数是f2函数的结果'func2';f3回调函数的参数,是Promise 实例f1的运行结果。
f1.then(f2)
.then(f3);
结果:
[Log] f2 – "func1成功"
[Log] f3 – "func2"
< Promise = $5
result: "func3"
status: "resolved"
“Promise”原型
结论:
f2会接收到Promise 实例f1返回的结果, f3回调函数的参数,是f2函数的运行结果。
微任务
Promise 的回调函数属于异步任务,会在同步任务之后执行。
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
上面代码会先输出2,再输出1。因为console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务执行。
但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
上面代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。
Promise.prototype.catch()
用于指定发生错误时的回调函数。
const promise = new Promise(function(resolve, reject) {
if(/*异步操作成功*/) {
resolve(res)
} else {
reject(new Error('test'));
}
});
promise.then(res => {
console.log(res);
}).catch(error => {
console.log('error=>', error);
});
一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
Promise.all()
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
Promise.race()
Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
网友评论