回调函数
对于一些比较耗时且不能确定完成时间的操作比如加载一张图片或者发送 AJAX 请求获取数据。在没有 Promise 之前,我们往往会使用回调函数。以 AJAX 请求为例说明事件回调的含义:初始化发送 AJAX 请求的时候,传入成功和失败的回调函数,当 AJAX 请求完成的时候执行回调函数。看代码:
function ajax(url, resolve, reject){
const xhr = new XMLHttpRequest();
//第三个参数为 true 表示是AJAX请求,JavaScript 执行线程不用等待返回的结果,只需要绑定回调函数即可
xhr.open('GET', url, true);
xhr.onload = ()=>{
if(xhr.status===200){
//成功的时候执行成功的回调
resolve(xhr.responseText);
}
}
xhr.onerror = ()=>{
//失败的时候执行失败的回调
reject();
}
xhr.send()
}
ajax("http://localhost:8081/getUser", (res)=>{
console.log(res);
}, error=>{
console.log(error);
})
Promise
Promise 可以理解为一个容器,里面保存着某个未来才会结束的事件的结果(通常是异步操作)。
Promise对象代表一个异步操作,有三种状态:pending(进行中)
resolved(已成功)
rejected(已失败)
。
基本语法:
const promise = new Promise(function(resolve, reject){
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
})
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve
和 reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
实质:当我们 new Promise(excutor) 的时候,Promise 的构造函数里会执行函数 excutor(resolve, reject); resolve 和 reject 是 Promise 内部的变量(函数类型)。
And 我们 new Promise(excutor) 是同步代码。
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
实质:resolve 和 reject 是 Promise 内部的变量(函数类型),是 Promise 暴露出来给用户根据异步操作的结果来决定 Promise 的状态。
- 异步操作成功,执行 resolve(res); 将执行结果放入 Promise 中
- 异步操作失败,执行 reject(error); 将失败原因放入 Promise 中
Promise 实例生成以后,可以用 then
方法分别指定 resolved
状态和 rejected
状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
当 resolve(res)
函数被执行后(Promise 的状态变为 resolved 的时候,then 方法的第一个函数才会被执行;当 reject(error)
函数被执行后 (Promise 的状态变为 rejected 的时候,then 方法的第二个函数才会被执行;
then
先介绍一下 then 函数的特点:
- 异步执行
- 链式调用
- 穿透传递 resolve 的数据 异步执行:then() 函数里面传递两个参数 res=>{}, error={} 分别表示 resolved 成功 和 rejected 的时候执行。如果 promise 的状态是 pending, then 里面的回调函数是不会执行的。
链式调用:then() 方法返回的是一个新的 promise
实例(注意,不是原来那个Promise
实例),所以我们才可以链式调用 then
方法。这个 promise
实例的状态不受原来 promise 执行状态的影响。默认 then() 方法返回的 promise
实例是成功的(resolved
),但是如果前面一个then()
方法返回的仍然是一个 promise
实例B(不是then()
方法默认生成的 promise
实例A),那么 then() 方法默认生成的 promise实例A 的状态与实例B的状态一致。说起来有点绕,我们来看代码:
//resolve 状态成功示例
const p = new Promise((resolve, reject)=>{
resolve("resolved");
console.log("constructor");
}).then(res=>{
console.log(res);
});
console.log(p);
输出结果:
//reject 状态失败示例
const p = new Promise((resolve, reject)=>{
reject("rejected");
console.log("constructor");
}).then(res=>{
console.log(res);
}, error=>{
console.log("error: "+ error);
});
console.log(p);
输出结果:
可以看到 then() 函数返回的是一个新的 Promise
实例,并且新 Promise
实例的状态默认是 fulfilled
。新 Promise
实例的状态不受原来 Promise
对象状态的影响。
const pp = new Promise((resolve, reject) => {
reject("pp rejected");
});
const p = new Promise((resolve, reject) => {
resolve("rejected");
console.log("constructor");
}).then(res => {
return pp;
}, error => {
console.log("error: " + error);
}).then(res => {}, error => {
console.log("then error: " + error);
});
console.log(p);
输出结果:
我们直接定义了一个状态是 rejected
的 Promise
实例 pp,作为 then 函数的返回值。那么第一个 then 函数的状态会同实例 pp
的状态保持一致。pp
状态是rejected
,所以会执行第三个 then 函数的拒绝的函数。
链式调用 then() 方法可以优化回调函数的“地狱回调”形式。每一次链式调用then() 方法都是顺序执行的。 以一个面试题来说明 链式调用 then() 方法的优雅:第1秒红灯亮,第2秒黄灯亮,第3秒绿灯亮,依次循环。看到这个题目我们很容易写出下面这样的代码:
function loop(){
setTimeout(()=>{
console.log("red");
setTimeout(()=>{
console.log("yellow");
setTimeout(()=>{
console.log("green");
loop();
}, 1000);
}, 1000);
}, 1000)
}
loop();
这么写就是很典型的“地狱回调”的形式了。代码不好维护,可读性也不高。接下来我们利用 Promise
链式调用的特点进行优化:
function light(color){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log(color);
resolve();
}, 1000)
})
}
function loop(){
light("red").then(res=>{
return light("yellow");
}).then(res=>{
return light("green");
}).then(res=>{
loop();
})
}
loop();
这样可以将回调函数嵌套的形式改写为 then() 方法的链式调用,代码看起来清爽了许多。后面会介绍 async 和 await 的写法,会更易于阅读。
穿透传递 resolve 的数据指的是我们空执行 then() 方法可以传递上一个 then() 方法的结果。看代码:
new Promise((resolve, reject)=>{
resolve(1);
}).then().then(res=>{
console.log(res); // 1
})
多个 then() 链式调用的时候,如果其中一个失败了,但是没有对应的处理函数,会导致后面的链式中断,不会执行。这一点同 async 和 await 语法是一致的。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p");
}, 1000)
}).then(res => {
console.log(res);
return Promise.reject("reject");
}).then(res=>{
console.log("then")
}).then(res=>{
console.log("last");
})
报错,链式调用终端,后面的代码不会执行。
但是如果我们处理了 rejected 情况,函数可以继续执行。但是通常的业务场景是需要根据前一个异步的结果才能进行下一步操作。
catch()
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
- 在 then 方法链式调用的时候,任何异常都可以在最后 catch 中处理,链式调用中断。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p");
}, 1000)
}).then(res => {
console.log(res);
return Promise.reject("reject");
}).then(res=>{
console.log("then")
}).then(res=>{
console.log("last");
}).catch(e=>{
console.log("reason: "+e);
})
- catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p");
}, 1000)
}).then(res => {
console.log(res);
return Promise.reject("reject");
}).catch(e=>{
console.log("reason: "+e);
})
.then(res=>{
console.log("then")
}).then(res=>{
console.log("last");
})
实际使用中,需要根据具体的业务场景来放置 catch 方法的位置。
Promise.resolve()
Promise.resolve()
可以将现有的对象转化为 Promise 对象:
Promise.resolve('foo')
//等价于
new Promise((resolve, reject)=>{
resolve('foo')
})
Promise.resolve()方法的参数分情况讨论。
- 参数是一个 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p");
}, 1000)
})
const pp = Promise.resolve(p);
pp.then(res=>{
console.log(res);
}, error=>{
// 输出 p
console.log("error: "+error);
})
- 参数是一个原始值。Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved。
const pp = Promise.resolve("resolved");
pp.then(res=>{
//输出 resolved
console.log(res);
}, error=>{
console.log("error: "+error);
})
Promise.reject()
与 Promise.resolve()
不一样, Promise.reject()
返回的新的 Promise 的实例,状态一定是 rejected
。Promise.reject()
方法的参数,会原封不动的作为reject 的理由,变成后续方法的参数。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p");
}, 1000)
})
const pp = Promise.reject(p);
pp.then(res=>{
console.log(res);
}, error=>{
console.log("error: "+error);
})
p作为参数被传递,输出 error: [object Promise]
Promise.all
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()
方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用 Promise.resolve()
方法,将参数转为 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的回调函数。
const p = new Promise((resolve, reject)=>{
setTimeout(()=>{
reject("p");
}, 1000)
})
const pp = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve("pp");
}, 200)
})
Promise.all([p, pp]).then(res=>{
console.log(res);
}, error=>{
console.log("error: "+error);
})
执行结果: 1s后输出 error p
const p = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve("p");
}, 2000)
})
const pp = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve("pp");
}, 2000)
})
Promise.all([p, pp]).then(res=>{
console.log(res);
}, error=>{
console.log("error: "+error);
})
执行结果: 2s后输出数组 ['p', 'pp']
Promise.race()
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()
方法的参数与Promise.all()
方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject("p");
}, 1000)
})
const pp = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("pp");
}, 200)
})
Promise.race([p, pp]).then(res => {
console.log(res);
}, error => {
console.log("error: " + error);
})
p
和 pp
谁先执行完,Promise.race 状态与它一致。上面代码执行结果是输出 pp
Promise.allSettled()
Promise.allSettled()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等所有这些参数实例都返回结果,不管是resolved
还是 rejected
,包装实例才会结束。该方法由 ES2020 引入。
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("p");
}, 1000)
})
const pp = Promise.reject("rejected");
Promise.allSettled([p, pp]).then(res => {
console.log(res);
})
实现 Promise
现在我们已经清楚的知道 Promise 的使用效果,那我们来试着写一个 MyPromise,实现 Promise 的功能。根据上面的示例,我们已经知道 MyPromise 应该有 state
属性来表示当前的执行状态;还有两个内置的函数 resolve
和 reject
分别代码完成和失败时执行的函数。then
方法需要返回一个新的 Promise
实例。
不考虑 then 链式调用的版本已经在之前的博客中介绍了 juejin.cn/post/689453…
这里会在此基础上进行加强,实现 then 方法的链式调用功能。下图是 MyPromise
代码的一个思维导图。
- 实现
then
方法异步执行的核心是,将then
函数的成功回调和失败回调记录在一个数组中,当resolve()
或者reject()
的时候再触发执行。 - 实现
then
链式调用的关键在于返回一个新的Promise
实例,这个实例默认状态是resolved
的;如果前一个 then 方法返回的是一个Promise
实例,那么默认返回的Promise
实例状态与返回的Promise
实例状态保持一致。
class MyPromise {
//定义 MyPromise 的三种状态
static PENDING = "pending";
static RESOLVED = "resolved";
static REJECTED = "rejected";
constructor(excutor) {
this.state = MyPromise.PENDING;
this.value = "";
this.reason = "";
//将成功的回调函数缓存在 resolvedCallback 中
this.resolvedCallback = [];
//将失败的回调函数缓存在 rejectedCallback 中
this.rejectedCallback = [];
let resolve = (value) => {
//只有 PENDING 状态才可以被修改,保证 MyPromise 状态的不可逆
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.RESOLVED;
this.value = value;
this.resolvedCallback.forEach(fn => fn(value));
}
}
let reject = (reason) => {
if (this.state === MyPromise.PENDING) {
this.state = MyPromise.REJECTED;
this.reason = reason;
this.rejectedCallback.forEach(fn => fn(reason));
}
}
try {
//同步执行传进来的 excutor 函数
excutor(resolve, reject);
} catch (e) {
console.log("excutor error", e);
reject(e);
}
}
then(onResolved, onRejected) {
//穿透传递
if (typeof onResolved !== "function") {
onResolved = value => value;
}
if (typeof onRejected !== "function") {
onRejected = reason => reason;
}
//返回一个新的 MyPromise 实例
return new MyPromise((resolve, reject) => {
// 异步代码,then 方法比 resolve 先执行的。回调函数要缓存起来
if (this.state === MyPromise.PENDING) {
this.resolvedCallback.push(() => {
const result = onResolved(this.value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
})
this.rejectedCallback.push(() => {
const result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
})
}else if (this.state === MyPromise.RESOLVED) {
//说明都是同步代码,resolve方法已经执行完了
//保证 .then() 方法异步执行,使用宏任务模拟。
setTimeout(() => {
const result = onResolved(this.value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
})
}else if (this.state === MyPromise.REJECTED) {
//说明都是同步代码,rejected方法已经执行完了
//保证 .then() 方法异步执行,使用宏任务模拟。
setTimeout(() => {
const result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
})
}
})
}
//resolve方法
static resolve(promise) {
return new MyPromise((resolve, reject) => {
if (promise instanceof MyPromise) {
//promise实例 原封不动
promise.then(resolve, reject);
} else {
resolve(promise);
}
})
}
//reject
static reject(promise) {
return new MyPromise((resolve, reject) => {
reject(promise);
})
}
static all(promises) {
return new MyPromise((resolve, reject)=>{
const result = [];
let count = 0;
promises.forEach((p, index)=>{
Promise.resolve(p).then(res=>{
result[index] = res;
count++;
if(count===promises.length){
resolve(result);
}
}, error=>{
reject(error);
})
})
})
}
static race(promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(item => {
Promise.resolve(item).then(res => {
resolve(res);
}, error => {
reject(error);
})
})
})
}
static allSettled(promises) {
const result = [];
return new MyPromise((resolve, reject) => {
promises.forEach(item => {
Promise.resolve(item).then(res => {
result.push(res);
if (result.length === promises.length) {
resolve(result);
}
}, error => {
result.push(error);
if (result.length === promises.length) {
resolve(result);
}
})
})
})
}
}
async & await
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。 async & await
可以理解为 Promise
使用的语法糖。
async
async
函数返回一个 Promise 对象async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。
async函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log('resolve', v),
e => console.log('reject', e)
)
//reject Error: 出错了
await
await
只能用在async
定义的函数中。await
后面跟 Promise 实例的话,返回的结果就是该实例返回的结果;如果是原始值,则直接返回。await
使得函数按照代码编写顺序执行。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
await 语句相当于调用 Promise 实例的
.then()
方法
async function f(){
await Promise.resolve("resolve");
await Promise.reject("rejected");
await Promise.resolve("resolve");
console.log("init");
}
f().then(res=>{
console.log("success: "+res)
}).catch(error=>{
console.log(error)
})
输出:rejected
分析:(1) f() 函数中有三个 promise 异步操作,一旦有一个异步操作失败,最后 f() 函数返回的 Promise 实例也是 rejected
。(2) 还有一点就是 async
函数中一旦发生异步操作失败没有被处理,同 then()
函数的链式调用一样会中断。
如果我们想要让执行不被中断的话,可以使用 try {} catche(e) {}
来处理:
async function f(){
await Promise.resolve("resolve");
try{
await Promise.reject("rejected");
}catch(e){
console.log("e", e)
}
await Promise.resolve("resolve2");
console.log("init");
}
f().then(res=>{
console.log("success: "+res)
}).catch(error=>{
console.log(error)
})
f()函数会依次执行,输出:
e rejected
init
success: undefined
实际使用中我们要根据业务需要放置 try {} catche(e) {}
的位置。
使用 async & await
优化红绿灯循环亮的面试题:
function light(color) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(color);
resolve();
}, 1000)
})
}
async function asyncLoop(){
await light("red");
await light("yellow");
await light("green");
asyncLoop();
}
asyncLoop();
使用细节
await 会让异步操作继发执行,而不是串行。如果两个异步操作没有先后执行的要求,我们需要小心使用:
function p() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("1");
}, 2000);
});
}
function pp() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("2");
}, 3000);
});
}
const start = new Date().getTime();
async function f() {
await p();
await pp();
const t = new Date().getTime() - start;
console.log("async", t)
}
f();
上面 p 和 pp 需要先后执行,p 请求需要2秒,然后 pp 请求3秒,一共需要 5秒。
如果 p 和 pp 本身无需先后执行,我们可以对上面的方法进行优化,缩短请求时间。
//方法一:使用 Promise.all() 让异步操作并行执行,耗时3秒,缩短操作时间
async function f() {
let res = await Promise.all([p(), pp()]);
const t = new Date().getTime() - start;
console.log("async", t)
}
f();
//方法二:
async function f() {
let p1 = p();
let p2 = pp();
await p1;
await p2;
const t = new Date().getTime() - start;
console.log(t);
}
f();
感谢
如果本文有帮助到你的地方,记得点赞哦,这将是我坚持不断创作的动力~