一、前言
大家好,我是疯狂的小波,一个热爱分享实战经验的前端开发。
这几天在开发功能时,调用同事写的函数,发现Promise
在部分场景下没有手动变更状态。大概长下面这个样子:
function test(flag) {
return new Promise((resolve, reject) => {
// do something
// ...
if (flag === 1) {
resolve()
} else if (flag === 2) {
reject()
}
});
}
于是就产生了下面这样的对话:
- 我:“调用
test
的时候, 如果这个flag
值是3
,没有手动设置状态,那不是也会返回一个值为undefined
的完成状态的promise
?”- 同事:“不是啊,如果是
3
,promise
不会往下执行。”- 我:“啊?我怎么记得没有手动处理也是默认返回完成状态的
promise
,只是值为undefined
。”- 同事:“???”
然后自己再仔细想想,是啊,没有手动处理不可能默认返回完成状态。Promise
内部状态的变更,大部分都是在异步操作中处理,这样的话,如果没有执行到变更状态的代码时,就已经是完成状态了?显然是不可能的。
正确的应该是没有执行到变更状态代码时,构造函数返回的 Promise
始终是待定状态。
这应该也是大部分人的正确认知。那我为什么会产生这种基础的错误认知?
在此之前我还特意看过 MDN
上 Promise
所有相关的文档,当时自认为对 Promise
已经足够了解了。没想到没过多长时间,一个这么基础的问题瞬间打回原形。
后来仔细想了想,让我产生这种误解的原因,还是理解的不够深刻,当时只是通过死记硬背的方式把文档内容记忆了下来,导致时间长了之后,记忆开始有点混乱了。
“没有手动返回,就返回完成状态的
promise
,只是值为undefined
”,这个是then
、catch
方法,以及async
函数返回值的其中一个规则,而不是构造函数的。
二、对 Promise
真的足够了解吗?
出了上面这个事情之后,再仔细想想,好像平常在用 Promise
进行开发的时候,有时也会遇到模棱两可或者拿不准的地方;再看看现在项目中的代码,发现也有很多不简洁、或者是使用错误的地方。
比如项目中的这种错误代码 ❌:
const wifiInfo = await getWifiInfo().catch();
还有这种不简洁的代码 😭:
async function getData() {
return await fly.post('/home/getData');
}
所以就把平常使用过程中存疑的、还有这种使用错误的地方,简单总结了下面几条:
Promise
在处理多个异步操作时,通常进行链式调用,如:p1.then().then().catch()
。那Promise
在链式调用时,是通过什么方式进行流转的?为什么可以通过链式的写法进行调用?- 为什么
.catch
可以捕获前面所有的异常? .catch()
与.catch(() => {})
对异常处理区别是什么?- 在使用
async/await
时,async
函数的返回值是什么?为什么async
函数具有传染性。 return await
和直接 return
是否一样?- 还有在涉及到多个
Promise
或async
方法嵌套调用时,可以准确判断函数的返回值吗?如果调用时其中有.catch
并被捕获,最终的返回值又是什么?
为了解决这些困扰,我把文档又全部重新理了一遍,并且结合demo
和Promise
的源码,总算把这些问题都搞清楚了。下面把我的解决过程和结论分享出来,如果在使用 Promise
的时候你也有过类似的困扰或其他的问题,相信一定会有所收获的。让我们以后使用 Promise
、async/await
时都能够快速、精准的做出判断,再也不被这些问题困扰了 💪💪💪。
三、简单的基础回顾
在此之前,需要先简单回顾下 Promise
的2
个基本内容:Promise状态
以及 Promise() 构造函数
。
我们后续的内容,基本都会基于这2
个来展开。
Promise状态
除了我们自己通过 Promise()
构造函数创建的 Promise对象
,通常我们使用一些第三方 API
,也会返回一个 Promise对象
(如 axios
)。
而 Promise对象
有3种状态。
- 待定(
pending
):初始状态,既没有完成,也没有失败 - 完成(
fulfilled
):操作成功完成 - 失败(
rejected
):操作失败
当由 pending
状态变更为 fulfilled
或 rejected
状态后,不会再变更。
Promise() 构造函数
包装不支持 Promise
(返回值不是 Promise
)的函数,返回一个 Promise对象
。常用于处理异步操作可能产生的不同结果。
new Promise((resolve, reject) => {
// do something
// ...
// 模拟异步操作,如接口请求
setTimeout(() => {
if (flag) {
resolve("完成传递的值") // fulfilled
} else {
reject("失败传递的值") // rejected
}
}, 0)
});
构造函数执行时,会返回一个 pending
状态的 Promise对象
,构造函数包裹的函数会立即执行。在异步执行到 resolve()
时,Promise对象
的状态会变更为 fulfilled
状态;执行 reject()
时,状态会变更为 rejected
状态。
注意:如果没有执行 resolve
、reject
函数,则返回的 Promise对象
始终为 pending
状态,后续的 then
、catch
、finally
回调方法也不会执行。
四、链式调用的根本原因:实例方法的返回值
then()
、catch()
、finally()
实例方法始终都会返回一个新的 Promise对象
。这也是为什么 Promise
能链式调用的根本原因。每次调用实例方法时,都会返回一个 Promise
,这个返回值就又可以继续调用实例方法了。
但是不同的实例方法、不同场景下,返回的 Promise
会有区别,这就导致在后续链式调用时,代码执行逻辑也会不一样。所以弄清楚实例方法的返回值至关重要。
这里 then()
、catch()
的返回值原则是一致的;finally()
有点区别。
.then()
、.catch()
.then(onFulfilled[, onRejected]);
.catch(onRejected);
then
方法接受2个参数:onFulfilled
是 Promise对象
变更为 fulfilled
状态的回调;onRejected
是 rejected
状态的回调(可选)。这2个函数都有一个参数,接受状态变更时传递的值。
.catch(onRejected)
等同于 .then(undefined, onRejected)
,只是它的一个语法糖,所以在返回值规则上,他们也是相同的。
返回值原则
1、如果 then
、catch
中的回调函数(onFulfilled
或 onRejected
):
- 1.1、返回一个值
A
,则实例方法返回 状态为fulfilled
、值为A
的Promise
; - 1.2、没有返回值,则实例方法返回 状态为
fulfilled
、值为undefined
的Promise
; - 1.3、抛出一个错误,则实例方法返回 状态为
rejected
、值为抛出的错误
的Promise
; - 1.4、返回一个
Promise(P)
,则实例方法返回 状态、值与P
相同的Promise
;P
状态变更时,这个Promise
也会变更。
示例如下:
const p = Promise.resolve("f1");
p.then(res1 => {
console.log(res1) // f1
return 'f2'
}) // 返回 fulfilled、值为f2 的Promise。继续执行下一个 then
.then(res2 => {
console.log(res2) // f2
}) // 返回 fulfilled、值为undefined 的Promise。继续执行下一个 then
.then(res3 => {
console.log(res3) // undefined
throw 'r1';
}) // 返回 rejected、值为r1 的Promise。继续执行下一个 catch
.catch(res4 => {
console.log(res4) // r1
return Promise.resolve("f3");
}) // 返回 fulfilled、值为f3 的Promise。继续执行下一个 then
.then(res5 => {
console.log(res5) // f3
}) // 最终返回 状态为fulfilled、值为undefined 的 Promise
async
函数的返回值原则,与上面的这些原则是一样的。所以弄清楚这些对我们用好async/await
也有很大的帮助。
2、如果 then
、catch
中没有对应状态的回调函数(或参数不是函数类型),那就会返回一个 与调用该方法的 Promise
相同的 新Promise对象
。
从表现上来看就是直接跳过没有对应状态回调的实例方法。比如调用 Promise.reject().then(onFulfilled)
时,由于 .then
中没有 rejected
状态的回调,.then()
方法会直接返回与 Promise.reject()
相同的 Promise
。
示例如下:
const p = Promise.resolve("f1");
p.then(res1 => {
// do 1
return Promise.reject("r1");
}) // 返回 rejected、值为r1 的Promise(P1)
.then(res2 => {
// do 2
}) // 由于这个then中没有rejected状态回调,这里do 2不会执行,then直接返回 与P1相同的 rejected、值为r1 的Promise
.catch(res3 => {
// 所以前面2个任意一个then中回调函数执行错误或返回rejected状态,都会执行这里
})
上面代码中,当执行 do 1
时返回一个 rejected
状态的 Promise(P1)
;执行到第二个 .then
时,由于没有 rejected
状态的回调,这个 .then()
会直接返回与 P1
相同的 Promise
,继续执行到 .catch
,则会进入 .catch
的回调。从表现上看,中间会直接‘跳过’ do 2
。
这也是为什么:我们在链式调用时只在最后写 .catch()
,之前的错误都能够捕获到;
同理,当我们希望某个操作不管是完成还是失败,都可以继续走后续的逻辑时,就可以将 .catch
写在指定的 .then
方法后,这样不管这个 .then
是否触发异常,也不会影响后续的链式调用。
const p = Promise.resolve("f1");
p.then(res1 => {
return doSomething();
})
.catch(res2 => {
// doSomething() 返回 rejected 时执行
return null;
})
.then(res3 => {
// 无论 doSomething() 返回什么,都会执行这里
// doSomething() 返回 fulfilled 时:上面的 .catch() 没有对应状态回调,也会直接返回,res3 值为 doSomething返回值
// doSomething() 返回 rejected 时:上面的 .catch() 捕获到异常,返回null,基于上面提到的返回值规则,此时 res3 值为 null
})
通常情况下,
Promise
中的这种链式调用使用async/await
会有更好的体验。
从 Promise
源码实现看 then
方法的返回值
为了进一步加深理解,我们从代码层面来看看,到底 Promise
内部是怎么处理的。
模拟 Promise
源码如下。为了方便理解,基本上每一步都加了注释,我们这里主要关注的是 then
方法的处理,在注释中也标识了上面返回值规则对应的代码处理:
class myPromise {
constructor(func) {
// 构造函数初始化时,返回的Promsie对象状态默认为 'pending' 状态
this.status = 'pending';
// 值默认为undefined
this.result = undefined;
// 完成状态回调函数数组
// 因为同一个Promise对象可以多次调用.then方法,当在pending 状态调用.then方法时,先在该数组中存储.then的完成状态回调函数
// 等到Promise状态变更为fulfilled时,再循环执行这些回调函数
this.onFulfilledCallbacks = [];
// 失败状态回调函数数组
// 原理和onFulfilledCallbacks类似
this.onRejectedCallbacks = [];
// 构造函数包装的函数,初始化时立即执行。
// 这里.bind(this),是为了指定调用resolve、reject方法时函数内this为当前实例,否则调用resolve、reject时获取不到函数内this的指向
func(this.resolve.bind(this), this.reject.bind(this));
}
// 将状态变更为fulfilled并赋值,如果在此之前绑定了完成回调则依次执行
resolve(result) {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.result = result;
this.onFulfilledCallbacks.forEach(callback => {
callback(result)
})
}
}
// 将状态变更为rejected并赋值,如果在此之前绑定了失败回调则依次执行
reject(result) {
if (this.status === 'pending') {
this.status = 'rejected';
this.result = result;
this.onRejectedCallbacks.forEach(callback => {
callback(result)
})
}
}
// 今天的主角,then实例方法
then(onFulfilled, onRejected) {
// 创建一个新的Promise对象,并最终返回
const returnPromise = new myPromise((resolve, reject) => {
// 如果调用.then方法时,已经是'fulfilled'状态,则直接异步执行回调
if (this.status === 'fulfilled') {
// 方法A:根据完成状态回调,设置 returnPromise 的状态及值
setTimeout(() => {
if (typeof onFulfilled === 'function') {
try {
let callBackResult = onFulfilled(this.result);
// 规则1.1、1.2、1.4 的集中处理,根据回调函数返回值决定 returnPromise 的状态及值
resolvePromise(returnPromise, callBackResult, resolve, reject);
} catch (e) {
// 规则1.3、onFulfilled回调抛出错误,返回rejected、值为抛出错误的 Promise
reject(e);
}
} else {
// 规则2、没有对应状态的回调函数,返回与调用者相同状态(因为这里是fulfilled状态,所以直接resolve)、值相同的 Promise
resolve(this.result);
}
});
} else if (this.status === 'rejected') {
// 方法B:根据失败状态回调,设置 returnPromise 的状态及值
setTimeout(() => {
if (typeof onRejected === 'function') {
try {
let callBackResult = onRejected(this.result);
resolvePromise(returnPromise, callBackResult, resolve, reject);
} catch (e) {
reject(e);
}
} else {
// 规则2、没有对应状态的回调函数,返回与调用者相同状态(因为这里是rejected状态,所以直接reject)、值相同的 Promise
reject(this.result);
}
});
} else if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
// 同上面方法A
// ...
});
this.onRejectedCallbacks.push(() => {
// 同上面方法B
// ...
});
}
})
// .then 实例方法始终返回1个新的Promise对象
return returnPromise
}
// 同.then方法,只是 .then(undefined, fn) 的语法糖,内部直接调用即可
catch(fn){
return this.then(undefined, fn);
}
}
/**
* 根据.then中回调函数返回值,决定.then实例方法返回的Promise的状态及值
* @param {promise} returnPromise .then方法返回的新promise对象
* @param {any} callBackResult .then中onFulfilled或onRejected回调函数的返回值
* @param {function} resolve returnPromise的resolve方法
* @param {function} reject returnPromise的reject方法
*/
function resolvePromise(returnPromise, callBackResult, resolve, reject) {
// 返回值不能是returnPromise本身,否则会循环引用
if (callBackResult === returnPromise) {
throw new TypeError('Chaining cycle detected for promise');
}
// 规则1.4、回调函数返回一个Promise,则.then方法返回的returnPromise与之状态、值同步
if (callBackResult instanceof myPromise) {
// 在Promises/A+规范中,如果callBackResult完成时返回的仍旧是一个promise(B),则returnPromise会与B的状态、值同步。所以这里用到了递归方法处理完成的返回值。
callBackResult.then(newResult => {
resolvePromise(returnPromise, newResult, resolve, reject)
}, reject);
}
// 规则1.1、1.2、回调函数返回一个值A、或没有返回值,.then方法返回fulfilled状态,值为A或undefined的Promise。
// 所以这里直接是以callBackResult作为returnPromise的完成值
else {
return resolve(callBackResult);
}
// Promise 中为了让链式调用的实现更具有通用性,规定:只要回调函数返回值暴露出一个遵循 Promises/A+ 协议的 then 方法,也会被当作Promise来处理结果。
// 所以在实现中,下面这个场景的判断,如果是一个含有.then方法的object或function,则会执行.then方法进行求值,与上面返回值是Promise时思路是一致的。
// 但是这种情况在日常开发中比较少见,这里为了方便理解,我把这段代码注释了,大家在这里只需要知道有这个机制就行。
// else if (callBackResult !== null && ((typeof callBackResult === 'object' || (typeof callBackResult === 'function')))) {
// try {
// var then = callBackResult.then;
// } catch (e) {
// return reject(e);
// }
// if (typeof then === 'function') {
// let called = false;
// try {
// then.call(
// callBackResult,
// y => {
// if (called) return;
// called = true;
// resolvePromise(returnPromise, y, resolve, reject);
// },
// r => {
// if (called) return;
// called = true;
// reject(r);
// }
// )
// } catch (e) {
// if (called) return;
// called = true;
// reject(e);
// }
// } else {
// resolve(callBackResult);
// }
//}
}
了解了源码之后,再来看之前的代码,是不是会觉得更加清晰了:从 Promise
实例初始化,到状态变更,再到调用 .then
方法的内部处理,以及不同状态下实例方法的返回值,最终根据这个返回值是怎么完成的链式调用。
可以再结合源码来看看下面这段代码:
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("疯狂的")
}, 1000)
});
p.then(res1 => {
return Promise.resolve(res1 + "小波");
})
.then(res2 => {
throw ('没给 ' + res2 + ' 点赞');
})
.catch(res3 => {
console.log(res3)
})
怎么样,如果结合源码的执行来看,是不是整体的感觉比之前更清晰了。我自己是感觉对 Promise
的掌握又更上一层楼了,至少不会在短时间内再忘记了~。你也点赞收藏下呗,免得时间长了之后忘记又找不到啦😊~
啥?为啥会抛出异常?看到这里了都不点个赞,不得出异常吗~
.finally()
.then
和 .catch
的使用在日常生活中非常多,所以分享的内容会多一点。下面我们再来看看另外一个实例方法 .finally()
。
.finally(onFinally);
在 Promise
结束后,无论结果是 fulfilled
或 rejected
,都会执行 onFinally
回调函数。与 .then
、.catch
不同的是,onFinally
回调函数没有参数。返回值也有所不同:
返回值原则
返回与调用该方法的 Promise对象
相同的 新Promise对象
。
// finally:返回 值为2、状态为rejected的Promise
Promise.reject(2).finally(() => {})
// then:返回 值为undefined、状态为fulfilled的Promise
Promise.reject(2).then(() => {}, () => {})
与上面的
then
、catch
中第2条规则:没有对应状态的回调函数时,返回原则是一样的。有点区别的是:如果在onFinally
回调中,抛出异常或返回rejected
的Promise
,则.finally()
会返回这个rejected
的Promise
,不过通常情况下并不会这样使用。
小结
Promise
基于 then()
、catch()
、finally()
实例方法返回一个 Promise对象
,达到了可以链式调用的效果。只要我们掌握这些不同场景下不同的返回值,对于 Promise
的链式调用,就可以很清楚的知道执行逻辑了。有些其他的框架也有这种设计思路,比如 Jquery
的实例方法,就是返回的 Jquery实例对象
,也是可以很方便的进行链式调用。
现在再看看刚开始总结的第1
、2
个疑问:
Promise
在处理多个异步操作时,通常进行链式调用,如:p1.then().then().catch()
。那Promise
在链式调用时,是通过什么方式进行流转的?为什么可以通过链式的写法进行调用?- 为什么
.catch
可以捕获前面所有的异常?
是不是现在感觉非常清晰了~,如果还有疑问,可以在评论区与我交流哦~。
顺便也可以想想第3
个问题:3..catch()
与 .catch(() => {})
对异常处理区别是什么?如果对上面的返回值规则理解了的话,应该就能发现区别了。
没有发现也没有关系,下面介绍异常处理时,我们再来揭晓这个答案。
五、链式调用的更优选择 async/await
当链式调用的链路比较长时,一般会使用 async
和 await
关键字来实现,代码会更清晰和简洁。
类似下面这样:
async function foo() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`这里是疯狂的小波,听到请回答: ${finalResult}`);
} catch(error) {
failureCallback(error);
}
}
async/await
中使用 try/catch
进行异常捕获,与 then/catch
类似。可以像上例中,统一进行捕获,这种中间任一个函数抛出异常或 rejected
,就会跳转到 catch
中执行。与 then/catch
中,最后一个 .catch
可以捕获前面所有异常效果是一样的。
使用 async
时,其中一个容易出错的也是函数的返回值。特别是嵌套调用等复杂场景,如果不能明确每一步的返回值,就容易产生很多问题。
async
函数返回值规则
始终返回一个新的 Promise对象。
1、与 then()
、catch()
的返回值规则相同。如果 async
函数:
- 返回一个值
A
,则返回 状态为fulfilled
、值为A
的Promise
; - 没有返回值,则返回 状态为
fulfilled
、值为undefined
的Promise
; - 抛出一个错误,则返回 状态为
rejected
、值为抛出的错误
的Promise
; - 返回一个
Promise(P)
,则返回 状态、值与P
相同的Promise
;P
状态变更时,这个Promise
也会变更。
示例如下:
async function foo() {
return 'f1' // == return Primise.resolve('f1')
} // 返回 fulfilled、值为f1 的Promise
async function foo() {
// 没有 return
} // 返回 fulfilled、值为undefined 的Promise
async function foo() {
return pormiseFunction()
} // 返回与pormiseFunction()返回值相同的Promise
2、await
等待的函数 抛出异常或返回 rejected
的 Promise
时。
没有异常捕获:则后续代码不会执行, async
函数直接返回 rejected
的 Promise
,值为抛出的异常值或 rejected
值。
async function foo() {
const result = await doSomething(); // doSomething 返回 rejected;
// 下面代码都不会执行
const newResult = await doSomethingElse(result);
return newResult;
}
foo() // 返回和 doSomething() 相同的 rejected 的 Promise
内部有异常捕获时:异常时,控制器执行 catch
块代码,再根据上面的第1
条规则返回
async function foo() {
try {
const result = await doSomething(); // doSomething 返回 rejected;控制器直接转到catch
// 下面代码都不会执行
const newResult = await doSomethingElse(result);
return newResult;
} catch(error) {
// 捕获异常。
return null;
}
}
foo() // 返回一个 fulfilled 的 Promise,值为 null
整体上来说,和 then()
、catch()
的返回值规则是差不多的。
async
函数返回值获取
有时 async
函数会直接返回一个值,如果习惯性的直接使用这个返回值,就会出错。
由于 async
函数的返回值是 Promise
, 所以获取返回值时也需要通过 Promise
的方式来获取。
async function foo() {
return 'f1'
}
console.log(foo() === 'f1') // ❌ async 函数返回的是一个promise对象
foo().then(res => console.log(res)) // ✅ f1。
或者再次使用 async/await
获取,这也是为什么通常说 async
函数具有传染性的原因。
async function getfoo() {
const result = await foo();
console.log(result);
}
return await
和单独 return
的区别
另外一个容易产生误区的地方就是 return await
和单独 return
,经常在使用过程中发现2
者都有使用,但是感觉效果又是一样的。下面我们就来看看他们到底有什么区别。
1、当有 try/catch
进行处理并且 await
的函数抛出异常时,这2
者有一些细微的差异。
如下,当 doSomething()
抛出异常或返回 rejected
时:
// 使用 return await
async function foo() {
try {
return await doSomething();
} catch(error) {
return null
}
}
// foo() 会进入catch回调,最终返回状态为fulfilled、值为null的Promise
// 使用 return
async function foo() {
try {
return doSomething();
} catch(error) {
return null
}
}
// foo() 会直接返回 rejected 的Promise,值为 doSomething() 抛出的异常或返回的 rejected 值
return await doSomething()
,将等待 doSomething()
执行出结果 (fulfilled
或 rejected
),如果是 rejected
,将会在返回前抛出异常。
而直接 return doSomething()
,不管 doSomething()
返回的 Promise
是 fulfilled
还是 rejected
都将会直接返回这个 Promise
本身
2、而没有 try/catch
时,使用是一样的。
async function foo() {
// do something
return await doSomething();
}
async function foo() {
// do something
return doSomething();
}
- 如果
doSomething()
正常执行,返回f1
,await
会进行求值,获取到f1
的值,由于async
函数会对返回值进行隐式替换,最终返回Promise.resolve(值)
,上面最终2种写法foo()
都会返回与f1
状态、值同步的Promise
; - 如果
doSomething()
返回异常r1
:同理,2种写法foo()
都会返回rejected
、值为r1值
的Promise
。
像这个例子中,如果中间没有其他的
await
关键字,使用async/await
就很多余了,就像最开始的例子中一样。可以直接写成普通函数就行。function foo() { // do something; return doSomething(); }
应该怎么记这种区别?
如果像上面举例这样记忆,就显得太复杂了,显然这不是一个好的方法。
其实我们只需要知道一点:await
关键字是获取 Promise
的 fulfilled
状态的值,如果 Promise
是 rejected
状态,则会直接抛出异常。
基于这个原则,再来看上面的例子,其他的代码都很熟悉,是不是能得到相同的结果。这个才是根本原因,上面的场景只是由这个原因导致的结果。
小结
我们现在再来想想上面提的第4
、5
个疑问:
- 使用
async/await
时,async
函数的返回值是什么?为什么async
函数具有传染性。 return await
和直接 return
是否一样?
如果能够立马想到答案,那说明对上面的内容基本上掌握啦 😄~
async
小技巧:同步开始,异步处理
既然已经说了这么多 async
的内容,再分享一个最近发现的一般人不会注意的小技巧。
常规的处理多个异步操作时,如果有依赖上一步的数据,通常都是链式调用。就像我们上面的例子中一样,等待上一个调用完成后,再进行处理下一个。
而有些场景下,异步操作在调用时没有依赖,但是在处理上有顺序要求。比如商品详情页2
个接口,获取商品详情信息、获取当前页面配置的促销信息,调用时没有数据依赖,但是只有当商品获取成功时才展示促销信息。此时接口调用就可以同步开始获取,进行异步处理,这样可以更快的让页面呈现出完整数据。如下:
- 常规的异步处理 - 页面需要
3s
呈现出完整数据:
async function getPageData() {
const detail = await getDetail(); // 获取商品详情,假设2秒后返回数据
setDetail(detail)
const promotion = await getPromotion(); // 等待上一个接口获取完成,再获取促销信息,假设1秒后返回数据
setPromotion(promotion)
}
- 同步开始,异步处理 - 页面
2s
呈现出完整数据:
async function getPageData() {
const detail = getDetail(); // 获取商品详情,假设2秒后返回数据
const promotion = getPromotion(); // 获取促销信息,假设1秒后返回数据
setDetail(await detail); // 2秒后赋值
setPromotion(await promotion); // 上面执行后立即执行
}
2
种方式最终实现的效果相同,第1
种方式页面完整呈现时间是2
个接口请求时间相加,而第2
种方式是最长的一个接口请求时间。在某些场景下还是比较有用的,需要的话快使用试试吧~。
六、常见错误 Uncaught (in promise)
还记得我们上面提到的这个疑问吧:.catch()
与 .catch(() => {})
对异常处理区别是什么?
在日常开发中经常看到这种 Uncaught (in promise)
错误,不管是我们自己写得代码,或者是第三方的代码,可能都会抛出这样的异常。就是当 Promise
变更为 rejected
时,没有被 catch
方法捕获到。
比如下面这样:
doSomething().then(function(result) {
// doSomethingElse
});
当 doSomething()
返回 rejected
状态时,由于没有 .catch
方法捕获异常,就会直接在控制台中抛出错误 Uncaught (in promise)
。
所以通常都要在链条最后加一个 .catch
来处理这种异常。
异常处理
有时为了避免抛出异常,或者不管 Promise
是完成还是失败时都继续向下执行,但是又没有其他的业务处理时,我们会简单的加一个空的 .catch()
。
❌ 错误处理:直接 .catch()
,不添加回调函数。
doSomething().then(function(result) {
// doSomethingElse
}).catch();
这样写并不会捕获到异常。还记得上面的返回值规则吗? .then()
、.catch()
的返回值是根据内部对应状态的回调函数决定的,单纯的 .catch()
没有回调函数,就会匹配其第2条原则:没有对应状态的回调函数,那就会返回一个 与调用该方法的 Promise对象
相同的 新 Promise
对象,所以这里当 doSomething()
返回 rejected
时,.catch()
后还是会返回 rejected
的 Promise
。所以最终还是抛出了 Uncaught (in promise)
错误。
✅ 正确处理:.catch(() => {})
async function foo() {
const result = await doSomething().catch(); // ❌ 直接.catch() 不会捕获异常。所以这里当rejected时,代码不会继续向下执行,foo() 直接返回 rejected 的 promise
const result = await doSomething().catch(() => {}); // ✅ catch有回调,可以捕获,doSomething() rejected 时,result 值为 undefined,代码继续向下执行
doSomethingElse(result)
}
全局异常捕获
那如果我们不希望每一个地方都去单独捕获,或者第三方内部的代码抛出这种错误,我们没有办法直接捕获的时候,应该怎么办?
可以通过 unhandledrejection
方法来全局捕获这种错误,然后在监听事件中做些自己的处理,比如需要做自定义的异常监控或问题收集时。
window.addEventListener("unhandledrejection", event => {
/* event.promise: 异常的promise对象
event.reason: 异常的 rejection 原因 */
// do something
event.preventDefault(); // 不将错误打印到控制台
}, false);
七、链式调用中的异步性
到了这里,我们开始时提出的几个疑问,基本都能找到答案了。
这一章想单独介绍下 链式调用中的异步性,也就是代码的执行时机(promise
在事件循环机制中的简单表现),感兴趣的也可以看看。
除了链式调用时的返回值,如果不清楚回调函数的执行时机,有时也会遇到各种问题。
promise
中的回调函数执行遵循以下3条基本原则:
- 当一个
Promise
状态变更为fulfilled
或者rejected
时,回调函数将被异步调用(由当前的线程循环来调度完成); - 即使是一个已经变成
rejected
状态的Promise
,传递给then()
的函数也会被异步调用; - 链式调用
.then()
的回调函数,会按照插入顺序进行执行。
如下,我们有主函数执行语句,主函数中的定时器,以及 Promise
的 then
回调。根据上面的原则,then
回调是异步调用,setTimeout
也是异步调用,并且优先级低于 then
回调。最终的执行顺序如下:
setTimeout(() => {
console.log('main setTimeout');
}, 0)
Promise.resolve(1).then(res => {
console.log(res);
})
console.log('main script')
// 依次返回:
// main script
// 1
// main setTimeout
上面的代码中,只有单一的 then
处理,如果是链式调用,执行顺序是什么样的?
Promise.resolve(1).then(res => {
console.log(res);
return res + 1
}).then(res => {
console.log(res);
return Promise.resolve(res + 1)
}).then(res => {
console.log(res);
return new Promise((resolve, reject) => {
console.log('start Promise')
setTimeout(() => {
console.log('then setTimeout');
resolve('then Promise')
})
})
}).then(res => {
console.log(res);
})
setTimeout(() => {
console.log('main setTimeout');
})
console.log('main script')
// 上面的代码会依次返回:
// main script
// 1
// 2
// 3
// start Promise
// main setTimeout
// then setTimeout
// then Promise
在第一个示例的基础上,我们添加了多个 then
的链式调用。如果上一个 then
返回的是一个 fulfilled
或 rejected
状态的 Promise
,则后续 then
的回调会继续执行;如果返回的是 pending
状态的 Promise
,则会等 Promise
的状态变更为 fulfilled
或 rejected
,再异步执行对应状态的回调。
还记得上面的 Promise
源码吗?如果结合源码再来看看这里的代码执行时机,这些文字描述,是不是变的更好理解了?
总结
扯了这么多,终于把开始提出的6
个疑问搞清楚了~。不过你确定真的都掌握了吗?会不会过一段时间像我一样又忘了,所以点个赞收藏一下呗~,记住不迷路。如果还有疑问或者有其他的问题,也欢迎评论留言与我交流 🎉🎉🎉。