基本使用
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); // Finallyp2.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); // areturn res + "b";}).then((res) => {console.log(res); // abreturn 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
