基本使用
ES6
新增的引用类型Promise
,可以通过new
操作符来实例化。创建时需要传入执行器函数作为参数。
new Promise(function (resolve, reject) {
// 实例化后立即执行,同步执行
console.log("promise");
});
Promise
是一个异步操作,是一个有状态的对象,可能处于如下 3 种状态之一:
- 待定(
pending
) - 兑现(
fulfilled
,有时候也称为“解决”,resolved
) - 拒绝(
rejected
)
待定是期约的最初始状态。在待定状态下,Promise
可以落定为代表成功的兑现状态,或者代表失败的拒绝状态。无论落定为哪种状态后就无法再改变。
如果想要将Promise
对象的状态切换为兑现或者拒绝的话需要调用执行器函数的relove
或reject
方法。
new Promise(function (resolve, reject) {
// 切换为兑现状态
resolve();
});
new Promise(function (resolve, reject) {
// 切换为拒绝状态
reject();
});
无论切换兑现还是拒绝状态后,Promise
的状态都不可以更改
let p = new Promise((resolve, reject) => {
resolve();
reject(); // 没有效果
});
setTimeout(console.log, 0, p); // Promise <resolved>
resolve
和reject
也可以传递参数,等待then
方法去处理。
let p1 = new Promise((resolve, reject) => {
resolve(123);
});
// 两个期约实例实际上是一样
let p2 = Promise.resolve(123);
注意,**resolve**
方法能够包装任何非**Promise**
值,包括错误对象,并将其转换为解决的期约。因此,也可能导致不符合预期的行为:
let p = Promise.resolve(new Error('foo'));
setTimeout(console.log, 0, p); // Promise <resolved>: Error: foo
与resolve()
类似,reject()
会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过try/catch
捕获,而只能通过拒绝处理程序捕获)。
let p1 = new Promise((resolve, reject) => reject());
// 两个期约实例实际上是一样的
let p2 = Promise.reject();
then()、catch()、finally()
Promise
的实例对象有三个方法用于处理resolve
和reject
的结果。
then()
方法接受一个或者两个参数,第一个参数表示resolve
兑现状态的时候执行,第二个参数表示reject
拒绝状态的时候执行。
let p1 = new Promise((resolve, reject)=>{
resolve("成功");
});
p1.then((res)=>{
console.log(res); // 成功
})
let p2 = new Promise((resolve, reject)=>{
reject("失败");
});
p2.then(
(res)=>{
console.log(res);
},
(error)=>{
console.log(error); // 失败
})
如果不想写兑现状态的处理函数,可以填为null
:
let p2 = new Promise((resolve, reject)=>{
reject("失败");
});
p2.then(
null,
(error)=>{
console.log(error); // 失败
})
catch()
方法表示reject
拒绝状态的时候执行,也就是then
方法的第二个参数
事实上,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null, onRejected)
let p2 = new Promise((resolve, reject)=>{
reject("失败");
});
p2.then((res)=>{
console.log(res);
}).catch(error=>{
console.log(error); // 失败
})
如果resolve
时发送错误,会更改promise
的状态为已失败,然后被catch
的第二个方法捕获:
let promise = new Promise((resolve, reject) => {
resolve(a);
});
promise.catch((res) => {
console.log(res); // a is not defined
});
finally
这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免onResolved
和 onRejected
处理程序中出现冗余代码。
let p1 = Promise.resolve();
let p2 = Promise.reject();
let onFinally = function() {
setTimeout(console.log, 0, 'Finally!')
}
p1.finally(onFinally); // Finally
p2.finally(onFinally); // Finally
状态依赖
当Promise
的状态依赖另一个Promise
状态的时候,前者Promise
的状态会失效,然后变更为后者Promise
的状态。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("fail"));
}, 3000);
});
// 当p2依赖p1的时候p2的状态无效,而是变更为p1的状态
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(p1);
}, 1000);
});
p2.then().catch((err) => {
console.log(err); // fail
});
微任务与宏任务
在JS
中异步代码中分为「微任务」和「宏任务」,两种任务都有自己的任务队列;微任务包括Promise
和Node
中的process.nextTick()
,宏任务包括除上面两种外其他的异步,例如setTimeout
。
每次事件轮询的时候,调用任务队列,这个时候存在优先级,优先执行微任务(微任务要比宏任务先执行)JS
异步代码执行的机制:
剖析 JavaScript 的执行机制
举例:
setTimeout(() => {
console.log("timeout");
}, 30);
let promise = new Promise((resolve, reject) => {
console.log(0);
resolve(1);
});
promise.then((res) => {
console.log(res);
});
console.log(2);
// 0 2 1 timeout
// 解析:
// 1、setTimeout执行,回调函数被挂起
// 2、Promise执行函数内部代码是同步执行,所以打印 0,然后resolve会被挂起
// 3、接着继续执行同步代码,打印 2
// 4、因为微任务Promise要先比宏任务setTimeout执行,所以先执行 then 方法,打印1
// 5、等then执行完成后,setTimeout的回调执行,打印 timeout
Promise.resolve().then((res) => {
console.log("promise1");
setTimeout(() => {
console.log("setTimeout2");
});
});
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve().then((res) => {
console.log("promise2");
});
});
// promise1 setTimeout1 promise2 setTimeout2
// 解析:
// 1、微任务Promise先执行,打印 promise1,然后setTimeout挂起
// 2、setTimeout 执行,打印 setTimeout1 ,然后Promise挂起
// 3、先执微任务,打印 promise2
// 4、最后执行 setTimeout2
实现 Thenable 接口
在ECMAScript
暴露的异步结构中,任何对象都有一个then()
方法。这个方法被认为实现了Thenable
接口。
如果对象上有then
方法,就可以直接调用resolve
方法,此时p
的状态由obj
对象来决定。
let obj = {
then: function (resolve, reject) {
resolve(123);
},
};
let p = Promise.resolve(obj);
// 第 9 行的 then 可以理解为第 3 行 resolve 的结果
p.then((res) => {
console.log(res); // 123
});
let obj = {
testThen: function (resolve, reject) {
resolve(123);
},
};
let p = Promise.resolve(obj);
p.then((res) => {
console.log(res); // {testThen: ƒ}
});
链式调用
then()
方法返回一个新的期约实例,如果没有提供then
处理程序,则Promise.resolve()
就会包装上一个期约解决之后的值。
如果没有显式的返回语句,则Promise.resolve()
会包装默认的返回值undefined
。
let p1 = Promise.resolve('foo');
// 若调用 then() 时不传处理程序,则原样向后传
let p2 = p1.then();
console.log(p2); // Promise {<fulfilled>: 'foo'}
// 这些都一样
let p3 = p1.then(() => undefined);
let p4 = p1.then(() => {});
let p5 = p1.then(() => Promise.resolve());
let p6 = p1.then(() => 'bar');
let p7 = p1.then(() => Promise.resolve('bar'));
let p8 = p1.then(() => Promise.reject());
setTimeout(console.log, 0, p3); // Promise {<fulfilled>: 'undefined'}
setTimeout(console.log, 0, p4); // Promise {<fulfilled>: 'undefined'}
setTimeout(console.log, 0, p5); // Promise {<fulfilled>: 'undefined'}
setTimeout(console.log, 0, p6); // Promise {<fulfilled>: 'bar'}
setTimeout(console.log, 0, p7); // Promise {<fulfilled>: 'bar'}
setTimeout(console.log, 0, p8); // Promise <rejected>: undefined
必须使用return
才能拿到值:
let promise = new Promise((resolve, reject) => {
resolve("a");
});
promise
.then((res) => {
console.log(res); // a
return res + "b";
})
.then((res) => {
console.log(res); // ab
return res + "c";
})
.then((res) => {
console.log(res); // abc
});
每个期约实例的方法(then()
、catch()
和finally()
)都会返回一个新的期约对象,而这个新期约又有自己的实例方法。这样连缀方法调用就可以构成链式调用。
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor');
setTimeout(resolve, 1000);
});
p1.then(() => new Promise((resolve, reject) => {
console.log('p2 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p3 executor');
setTimeout(resolve, 1000);
}))
.then(() => new Promise((resolve, reject) => {
console.log('p4 executor');
setTimeout(resolve, 1000);
}));
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)
如何把生成期约的代码提取到一个工厂函数中,就可以写成这样:
function delayedResolve(str) {
return new Promise((resolve, reject) => {
console.log(str);
setTimeout(resolve, 1000);
});
}
delayedResolve('p1 executor')
.then(() => delayedResolve('p2 executor'))
.then(() => delayedResolve('p3 executor'))
.then(() => delayedResolve('p4 executor'))
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)
all() 和 race()
Promise.all()
静态方法创建的Promise
会在一组Promise
全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。
let p1 = Promise.all([
Promise.resolve(),
Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve() 转换为期约
let p2 = Promise.all([3, 4]);
// 空的可迭代对象等价于 Promise.resolve()
let p3 = Promise.all([]);
// 无效的语法
let p4 = Promise.all(); // TypeError: cannot read Symbol.iterator of undefined
如果所有Promise
都成功解决,则then
方法的参数就是所有解决的数组,按照迭代器顺序:
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
]);
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4]
如果有一个包含失败状态的Promise
,则合成的Promise
也会失败,第一个失败的Promise
会将自己的理由作为合成Promise
的拒绝理由:
// 一次拒绝会导致最终期约拒绝
let p2 = Promise.all([
Promise.resolve(),
Promise.reject("失败"),
Promise.resolve()
]);
setTimeout(console.log, 0, p2); // Promise {<rejected>: '失败'}
p2.catch((error)=>{
console.log(error); // 失败
})
Promise.race()
和all()
基本类似,也返回一个包装Promise
,不同的是race
不管任意一个Promise
先完成状态的落定就会处理。
let p1 = Promise.race([
Promise.resolve(),
Promise.resolve()
]);
// 可迭代对象中的元素会通过 Promise.resolve() 转换为期约
let p2 = Promise.race([3, 4]);
// 空的可迭代对象等价于 new Promise(() => {})
let p3 = Promise.race([]);
// 无效的语法
let p4 = Promise.race(); // TypeError: cannot read Symbol.iterator of undefined
无论是解决还是拒绝,只要是第一个落定的Promise
,Promise.race()
就会包装其解决值或拒绝理由并返回新Promise
:
// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([
Promise.resolve(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
]);
setTimeout(console.log, 0, p1); // Promise <resolved>: 3
// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([
Promise.reject(4),
new Promise((resolve, reject) => setTimeout(resolve, 1000))
]);
setTimeout(console.log, 0, p2); // Promise <rejected>: 4
// 迭代顺序决定了落定顺序
let p3 = Promise.race([
Promise.resolve(5),
Promise.resolve(6),
Promise.resolve(7)
]);
setTimeout(console.log, 0, p3); // Promise <resolved>: 5