Promise 期约
Promise
期约之前
回调地狱
设想这样一个经常发生的场景,我们希望处理Ajax请求的结果,所以我们将处理请求结果的方法作为回调传入,需要将请求结果继续处理,这就导致我们陷入了回调地狱
doSomething(function(result) { // doSomething的结果以回调调用的形式传入doSomethingElse
doSomethingElse(result, function(newResult) { // doSomethingElse的结果以回调调用的形式传入doThridThing
doThirdThing(newResult, function(finalResult) { // doThridThing的结果以回调调用的形式最终被console.log调用
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);
而Promise的链式调用特点可以将需要链式处理的结果串联起来
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
快速入门
Promise就像现实生活中的约定一样
- 我们交给朋友一个任务,朋友给你他们要去完成任务的保证或者叫期约,然后你就可以干别的了,朋友继续帮你完成任务
- 你的朋友既可能完成那个保证或叫期约,中间也有可能出现错误导致任务的失败
- 成功时你会想知道成功的结果,失败时你会想知道失败的原因
- 成功了你会有成功后的行为比如说请朋友吃顿饭,失败了你会有失败的行为比如让朋友请你吃顿饭
- 最后你会想有一个最终的行为,比如说记个笔记说这次任务完成的怎么样,无论成功和失败你都想做的东西
对应Promise的过程
- 拜托朋友完成指定任务和朋友答应你去完成任务的过程对应Promise的创建,也就是
new Promise
- 朋友成功完成任务通知你成功的过程对应Promise中调用
resolve()
,失败则对应Promise中调用reject()
- 而成功与失败的原因,对应调用
resolve()
和reject()
时传入的参数,这个参数会传入最终的成功处理器、失败处理器中 - 任务成功或失败后你的行为对应Promise中的成功处理器与失败处理器,这是两个函数,里面可以放一些成功时候的操作和失败时候的操作
- 最终的行为对应Promise中的最终处理器
对应代码,下面举了一个执行setTimeout的例子,这里的任务就是定时器任务
const promise = new Promise((resolve, reject) => {
/**
* 安排异步任务,通常调用一些运行环境的异步API,例如IO操作和Ajax操作
* 同时为异步任务绑定resolve()和reject(),当异步任务成功和失败时调用他们
**/
setTimeout(() => {
resolve({message: "Promise keeped"}); // 这里直接成功resolve
}, 10 * 1000);
});
// promise.then()中第一个参数为成功处理器,第二个参数是失败处理器
promise.then((result) => { // 处理器的第一个参数是对应resolve()和reject()中传入的
console.log(result.message); // 打印"Promise keeped"
});
promise.finally(() => { // 绑定最终处理器
console.log("3-day holiday has begun");
});
/** 最终控制台输出结果
* "Promise keeped"
* "3-day holiday has begun"
*/
看完上面的代码后还是没有理解完整Promise逻辑没关系,下面详细介绍Promise的细节
详细描述
Promise的三个状态
- 待定(pending):初始状态,此时任务已被安排,也就是任务进行当中,还没得到结果
- 兑现(fulfilled):任务成功完成,Promise就会变成这个状态
- 拒绝(reject):任务失败
使用构造函数new Promise
创建时Promise默认在待定状态,直到转换为兑现或拒绝状态,这个状态变化是单向且仅可变化一次。从待定状态到成功状态是通过调用resolve()
实现的,转换到失败状态则是通过调用reject()
实现的,这两个回调会通过构造函数中第一个函数参数传入。
下面是创建一个待定态并直接跳过安排任务步骤直接转换为兑现态的代码
let promise = new Promise((resolve, reject) => {
resolve();
});
console.log(promise); // Promise {<resolved>: {...}}
下面是创建一个待定态并直接跳过安排任务步骤直接转换为拒绝态的代码
let promise = new Promise((resolve, reject) => {
reject();
});
console.log(promise); // Promise {<reject>: {...}}和Uncaught (in promise)错误
了解了基本的Promise任务转换之后,让我们引入需要真正安排异步任务的操作,这才是Promise安排任务的真正用法(虽然这段代码用处依然不算大,但足够展示Promise用法了)
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 10 * 1000);
});
console.log(promise); // Promise {<Pending>: {...}}
在上方的代码中,我们安排了一个异步任务也就是setTimeout,一旦定时器结束调用了resolve()
,promise将立刻变为resolved的状态。同样道理可以为异步操作绑定Promise的失败状态调用reject()
来将Promise状态转换为rejected
。
在没有Promise以前,我们不能拿到定时器的执行状态,现在有了Promise的帮助,我们可以看到在setTimeout()
的10秒定时没有完成之前,promise将会是pending的状态。
那么给朋友安排任务、朋友执行任务然后将结果通知给你这两个步骤都完成了,接下来就是朋友将成功的结果或失败的原因传递给你、你根据朋友传过来的成功结果或失败原理做对应处理的过程了
Promise构造函数的resolve()
和reject()
传入参数就是朋友告诉你成功结果和失败原因的途径,这两个函数传入的参数,会对应传递给成功处理器和失败处理器
Promise的状态转换会触发处理器函数的执行,对应你对朋友处理成功或失败的结果做出相应的反应
在继续这两个过程之前,我们再宏观的看一下Promise的三个重要使用过程
Promise的四个重要的过程
- 安排异步任务
- 成功、失败的调用绑定
- 成功、失败、最终处理器的绑定
- 成功、失败、最终处理器执行
上述四个过程中前三个需要我们进行编码,最后一个过程在异步事件完成后被触发。构造过程在上节详细描述过了,构造过程是安排异步任务、为异步任务绑定Promise的成功、失败转换调用(也就是resolve
和reject
)的过程。
成功、失败调用的绑定,是创建完Promise之后的过程,用于绑定成功处理器与失败处理器给Promise,在异步操作执行完之后,成功或失败处理器将被调用做得出结果之后的处理,这个绑定操作可以通过Promise.prototype.then
和Promise.prototype.catch
来实现。
Promise.prototype.then
接受两个参数,一个是成功处理器,一个是可选的失败处理器,成功、失败处理器各自可以接受一个参数,这个参数将是异步操作完成回调resolve()
或reject()
传入的参数。
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ message: "Promise Resolved"});
}, 10 * 1000);
});
promise.then((value) => {
console.log(value.message); // "Promise Resolved"
});
Promise.prototype.catch
只接受一个失败处理方法作为参数,这个失败处理器将会被传入reject()
传入的值
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject({ message: "Promise Rejected"});
}, 10 * 1000);
});
promise.catch((value) => {
console.log(value.message); // "Promise Rejected"
});
所以四个过程的任务
- 安排异步任务:在Promise构造函数中创建一个期约并安排期约的异步任务给朋友(异步API),你则持有这个期约用于之后绑定成功/失败处理器
- 成功、失败调用的绑定:在Promise构造函数中给朋友(异步API)绑定上成功时的
resolve()
和失败时的reject()
用于异步操作执行完成后转换期约的状态、调用成功/失败处理器、传入异步操作处理完成的数据 - 成功、失败、最终处理器的绑定:使用你持有的期约上的
then()
、catch()
、finally()
来绑定成功、失败、最终处理器,成功、失败处理器会在Promise进入对应状态时被调用 - 成功、失败、最终处理器的调用:promise状态在异步操作结束后调用
resolve()
或reject()
转换为fullfilled
或rejected
状态从而触发已经绑定的成功处理器或失败处理器,无论达到以上两个之中的哪个状态,最终处理器都会被调用
成功、失败、最终处理器的绑定有两种方式,单独绑定和链式绑定,为promise绑定则处理器,处理器会按顺序执行;而使用链式绑定,每个then()
、catch()
、finally()
都会返回Promise,即可以实现链式调用
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("some value");
}, 1000)
});
promise.then(value => {
console.log(value); // "some value"
}).then(value => {
console.log(value); // undefined
return "other value";
}).then(value => {
console.log(value); // "other value"
});
then()
、catch()
、finally()
中默认返回的Promise是开头绑定的Promise,但如果在处理器中显式返回了值则将返回被Promise.resolve()
包裹的该值,也就是向下传递的Promise被换成了上一个处理器返回的内容Promise(上方代码第二、三个处理器展示了这种情况)。
链式处理时在处理器中抛出错误则会返回一个Promise.reject()
传递给下方的处理器,若此异常未被后面绑定的catch()
处理异常将在控制台抛出。
组合Promise
Promise异步操作也可以组合执行,ECMAScript提供了Promise.all()
与Promise.race()
方法实现Promise的组合执行。组合执行Promise有许多场景,比如我们需要多个互不依赖的异步请求之后将所有请求结果汇总与拼合,我们就可以用到Promise.all()
;而Promise.race()
为我们提供了一个拿到多个异步操作最快的那一个操作返回值的能力。
Promise.all()
用于将多个用于安排异步任务的Promise并行执行,返回带有所有任务执行结果数组的Promise,当其中一个Promise出错则立即创建并返回一个带有这个错误信息的reject
状态的Promise
let promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("First Promise");
}, 1000)
});
let promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Second Promise");
}, 2000)
});
Promise.all([promise1, promise2]).then(values => {
for (const value of values) {
console.log(value); // 大约2s之后打印: First Promise Second Promise
}
});
将上方的Promise.all()
改写为Promise.race()
将只会返回最早执行完成的异步任务的值
// ....
Promise.race([promise1, promise2]).then(value => {
console.log(value); // 大约1s之后打印: First Promise
});
async / await
async/await关键字是ECMA 2017(ES7)引入的Promise语法糖,它让Promise任务的调用变得简洁,让异步代码看起来不那么难理解。当JavaScript引擎处理执行到async
修饰的方法,该方法会被推迟到其他同步代码执行完毕之后执行。
下面是一个例子,简单看一下例子之后会进一步解析async/await
// 下方方法返回一个安排了异步http请求的Promise,返回的Promise带有请求返回值
function request(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get(url, res => {
res.on('data', chunk => {
resolve(chunk.toString());
});
})
}, 1000); // 为了让效果明显一点
});
}
// 上节学到的请求
function getInfo() {
request("http://localhost:3000/info").then(result => {
global.info = result;
});
// 没有办法直接在方法内获取promise执行结果
}
// 使用async await
async function getUser() {
// 可以直接拿到promise执行结果
let result = await request("http://localhost:3000/user");
global.user = result;
return result;
}
getUser().then(result => {
console.log(result);
});
async/await的本质是将原本在处理器内部编写的代码以同步的写法与Promise组合起来。在上方例子的使用async await注释下方代码中,我们以同步的方式执行了request异步任务,并将返回值绑定在全局变量user上,接下来我们返回了result并使用.then
绑定了成功处理器,处理器中打印了result的值。
其实上方描述中async/await中执行的绑定代码效果,与给在request异步任务后绑定一个执行同样操作的处理器效果一致,也就是上方代码其实也可以这样实现
request("http://localhost:3000/user").then(result => {
global.user = result;
console.log(result);
});
那async/await的好处在哪,这种写法的好处就是当你需要链式调用Promise(需要区别于Promise的链式调用功能)时,这种写法会十分的整洁。
// 省略上方request的代码
request("http://localhost:3000/info").then(result => {
global.info = result;
return request("http://localhost:3000/base");
}).then(result => {
global.base = result; // 这里的result是请求/base获得的
return request("http://localhost:3000/user");
}).then(result => {
global.user = result;
});
// 上方代码可以简写为
async function getVariables() {
global.info = await request("http://localhost:3000/info");
global.base = await request("http://localhost:3000/base");
global.user = await request("http://localhost:3000/user");
}
getVariables();
aynsc/await关键词不仅可以应用在普通方法上,还可以用在其他方式声明的方法上
// 一般方法声明
async function foo() { }
// 匿名函数
let bar = async function() { }
// 箭头函数
let baz = async () => { }
setTimeout(async () => {
global.res = await fetch("resource.json");
}, 1000);
// 类成员函数
class Quz {
async qux() { }
}
由于async/await
函数体内代码同步执行,错误处理使用try/catch
正常捕获就可以。
总结
- Promise是一个期约,它不是异步操作本体,而是一个方便你进行同步操作(安排异步任务、绑定成功失败处理器)的一个中介
- Promise有三个状态,创建与异步操作未完成时状态为pending、操作完成状态转换为resolve、操作失败状态转换为rejected,后两个状态的Promise可以分别通过
Promise.resolve()
和Promise.reject()
直接创建 - Promise的使用过程有四步,前两步为同步操作,最后一步为异步操作
- 安排异步任务并建立期约,(
new Promise((resolve, reject)=>{ })
并在箭头函数中为异步任务绑定成功时的回调(resolve
)和失败时的回调(reject
),Promise的状态转换通过异步任务回调这两个函数来实现 - 绑定处理器,为上一步建立的期约Promise绑定成功、失败处理器,这一步通过
.then()
或.catch()
实现 - 异步任务执行,被浏览器或Nodejs环境自动执行。异步任务比如
setTimeout
、http请求等 - 异步任务执行完毕执行处理器,异步任务执行完毕浏览器或Nodejs环境会执行对应的第一步绑定的
resolve()
或reject()
转换Promise的状态,在Promise状态转换后环境会自动将对应处理器安排到微任务队列等待Event Loop安排执行
- 安排异步任务并建立期约,(
- 一个Promise可以绑定多个处理器,处理器按照绑定顺序执行,默认为所有处理器传入
resolve()
回调传入的值。处理器也可以返回值,返回值会传入下一个处理器 - 如果在链上任意位置报错,则可以使用绑定的失败处理器进行处理,若不处理错误将抛给控制台导致程序运行问题
- 使用
Promise.all()
一次性为多个Promise绑定处理器,所有异步操作传入resolve()
的值将组合为数组传递给成功处理器,如果有任意异步操作调用了reject()
则传入的单个失败信息将会传给失败处理器。Promise.race()
则提供了一个谁异步操作执行的最快处理器拿到谁的结果的方法 async/await
是ES7推出的Promise语法糖,用于将Promise处理器内代码以同步的方式写在方法中让代码看起来更简洁