Promise应用场景总结

2,148 阅读8分钟

场景描述

在我最近的开发中,遇到了下面几个场景:

  1. 一个区块的渲染需要拉取多个接口,如何摆脱闪屏,保证良好的保证用户体验?
  2. 接口的数据量巨大,处理逻辑臃肿,代码混乱,后续维护难怎么办?
  3. 业务逻辑特殊,如何优雅地给项目中的某个接口设置可控制的超时重试机制?

乍一看,它们都是基于业务遇见的不一样的应用场景。然而,在实际开发中,他们都和Promise异步编程有着不可分割的关系。在真正解决了上面的问题之后,不难发现,解决方案其实都是基于一些基础的Promise知识来进行二次拓展的。

为了能够加深对Promise及其常用静态方法的理解,在日后遇到相似问题时提高解决效率,我总结了这篇文章。希望在能给到自己和更多的同学一些知识积累和启发。

一、项目应用场景

场景1. 同时拉取多个接口

我第一次遇到这个问题是在一个需求中,我们需要实现如下效果:

  1. 点击“进行中”Tab,显示Loading状态

  2. 前端同时拉取 "答题中" 和 “未考”的两个考试接口。结果都都返回后,把“答题中”的数据放在最顶。

  3. 取消Loading状态,展示列表

1.png

当组件的渲染需要多个接口的数据时,因为接口的返回时长往往不一致,用同步代码来发送请求会导致问题。问题往往是部分接口先得到返回结果,并进行渲染,其他接口后得到返回结果再次更新视图,导致页面有“闪屏”的情况,用Promise.all()可以很好地避免此类问题:

2.png

但值得注意的是,使用Promise.all()需要等待所有传入的Promise的状态都变为Fulfilled,或者其中一个Promise的状态变为Rejected时,代码才能进入到Promise.all的回调方法中去。此时页面的渲染时间为所有接口返回时间的最大值。

拓展: 还有另外一个值的考虑的问题是:一旦有传入的Promise状态变为Rejected了,且没有自己的catch方法,Promise.all就会进入catch回调。这使得有时候仅仅只是一个无关紧要的接口挂掉了,但因为我们使用了Promise.all(),导致整个页面都无法渲染出来。通过给每个传入的Promise增加catch方法,我们可以做到把多个请求合并在一起,哪怕有的请求失败了,也返回给我们,我们只需要在一个地方处理这些数据和错误的逻辑即可。

3.png

4.png

经过上述改动后, 传入的Promise在请求接口失败会首先会把我们自定义的错误信息Rejected出去。而我们给传入的Promise定义了自己的catch方法,该方法会返回一个新的Promise实例,在执行完catch方法后,也会变成Resolved。这样,Promise.all()方法参数里面的两个实例都会Resolved,因此我们可以在then方法中统一处理数据与错误的逻辑。

经过上述的处理后,使用Promise.all()时,就再也不用担心因为一个请求失败导致整个页面都无法渲染数据了。


 补充: 实际上, 为了解决上述提到的Promise.all()在使用中的问题,ES2020 引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了。相较于Promise.all(),它最大的优点是:无论参数实例Resolve还是RejectPromise.allSettled()都会执行then方法的第一个回调函数,而不会catch到参数实例的Rejected状态。

5.png

同时,感谢各位同学提醒,axios本身返回Promise,无需在外部进行进行额外的Promise包装。但目前Promise.allSettled()依然处于TC39第4阶段草案,使用时仍需注意兼容性的问题,附上兼容性对比图:

6.png

7.png


场景2. 数据量超大的接口回调处理

我们在上面已经说明了如何改进Promise.all()的使用方法,使得我们可以在Promise.all().then()回调中同时处理接口拉取的数据以及相关错误逻辑。

然而,我们会发现,当我们需要同时拉取的接口数越来越多时,Promise.all().then()回调中需要写的逻辑也就会越来越臃肿。即使上面仅仅是处理两个简单的接口,其if/else逻辑就已经给我们造成了很大的阅读障碍了。然而,这类问题实际上不仅仅只有Promise.all()才会引起的,当一个接口返回了大量我们需要做不同处理的数据时(比如把上面的两个或多个接口合并为一个),我们也需要考虑如何才能优化此处代码的可阅读性。

实际上,我们可以通过灵活地运用Promise.prototype.then(),来实现一个中间件功能,以分割不同的数据处理逻辑:

8.png

通过用Promise.prototype.then()作为中间件,我们的代码能够清晰地分成不同的逻辑处理区块。甚至,我们还可以把不同的逻辑处理封装成函数。这样,整个函数的逻辑会更方便自己和其他后续的维护者去阅读。

场景3. 接口重试实现

为了给予用户足够好的体验,在项目的特定场景中,我们会做一些接口超时重试的操作。比如用户正在地铁上浏览一门课程,该课程需要记录用户的学习时长。当网络短暂不佳的时候,我们就需要重试机制,来在用户无感知的情况下重新请求学习时长记录接口,来避免出现网络短暂不可用导致用户学习无法记录的情况。

当然,这种超时重试的机制应该在各大AJAX库都有实现。 但在我们使用fetch等原生的Web API时,无法掌握接口重试的实现就会有点麻烦了。实际上,Promise.race()的灵活使用能够让我们轻松实现一个接口重试逻辑:

9.png

10.png 上述代码中,我定义了一个timeout函数与一个请求request函数。同时定义了一个ajax函数,用于传入请求url,超时时间以及重试次数。如果request的接口在超时时间内依然没有返回,Promise.race()将会被我们定义的timeout函数Rejected掉。通过上述场景1和场景2说的方法,给传入的Promise定义catch函数,我们可以在同一个地方判断Promise.race中最终Resolved的原因,来执行请求成功返回/超时对应的逻辑。

二、拓展应用场景

场景1. 批处理请求

  1. 加载图片的最大并发数为maxNum

  2. 每当有一张图片加载成功/失败,就腾出一个空位,可以加载剩余未加载的图片

  3. 所有图片加载完成后,结果按照加载顺序依次打出

这个场景来自 Chris Jensen , 基于他写的一个有趣的Promise.race()方法用例,  我也试着来模拟批量加载图片:

11.png

场景2. 最快地获取资源

Promise.all()相反,Promise.any() 接收一个Promise可迭代对象,只要其中的一个Promise成功,就返回那个已经成功的 promise。因此,我们可以利用这个特点来获取第一张成功加载的图片。如一些需要随机显示一张图片的场景,就可以一次性加载多张,再利用Promise.any()获取最快记载好的图片,以提高首屏渲染速度。再如你有多台服务器,则可以使用Promise.any()从响应速度最快的服务器检索需要的资源。

12.png 注意! Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。

三、总结

1. Promise.race():

  • 常用于按需取消Promise的场景,搭配一个定时器传入Promise.race()方法,做到超时打断/重试的效果
  • 用于批处理操作,需要维护一个promises数组,用race()方法及时处理最快掉resolvedPromise。为后续等待队列中的任务腾出位置

2. Promise.all()

  • 基本用于渲染时同时需要多个接口数据/前端聚合数据的场景,按需地给传入的Promise参数定义catch函数,能够方便我们在同一个地方处理数据渲染/接口错误逻辑,避免因为一个小接口导致整个页面都无法渲染出来的情况。

3. Promise.prototype.then()

  • 灵活使用then()作为中间件,用于分割不同的代码逻辑。能改避免在回调函数中,业务逻辑过于臃肿导致难以维护的情况。
  • then()的链式调用同时适用于简化接口之间相互依赖的情况,但async/await无疑在大部分情况下更胜一筹。

4. Promise.any()

  • 仍未被完全支持,兼容性较低,暂时只能用于“从最快的服务器检索资源”等小众场景,慎用,先了解即可。