用了几年的Promise,竟然还搞不清楚返回值是什么~ 您所在的位置:网站首页 boolean方法返回值 用了几年的Promise,竟然还搞不清楚返回值是什么~

用了几年的Promise,竟然还搞不清楚返回值是什么~

2024-03-26 08:37| 来源: 网络整理| 查看: 265

一、前言

大家好,我是疯狂的小波,一个热爱分享实战经验的前端开发。

这几天在开发功能时,调用同事写的函数,发现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个疑问搞清楚了~。不过你确定真的都掌握了吗?会不会过一段时间像我一样又忘了,所以点个赞收藏一下呗~,记住不迷路。如果还有疑问或者有其他的问题,也欢迎评论留言与我交流 🎉🎉🎉。

往期推荐 👉 成为Vue高手,必须掌握的37个知识点 🔥 👉 React性能优化:一文看懂优化原理及方案 🚀


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有