跳到主要内容

Promise

  • 解决回调地狱嵌套问题
  • Promise 是异步编程的一种解决方案
  • Promise对象是一个构造函数,里面有all、reject、resolve方法
    • Promise.reject(),Promise.resolve()..
    • 在原型对象prototype中,有catch、then、finally方法
      • Promise.prototype.catch()
      • Promise.prototype.then()
      • 所以在Promise实例上都具有这些方法,可以调用这些方法
  • promise是一个对象,从它可以获取异步操作的消息
  • promise有三种状态:pending(等待),fulfilled((成功),rejected(失败)

特点:

  • 1、状态一旦改变,就不会再变。创建promise实例后,它会立即执行,无法中断
    • Promise 新建后立即执行,Promise对象,可以将异步操作以同步操作的流程表达出来
    • 简单来说:Promise本身是同步的立即执行函数
  • 2、then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行
    • promise.then里的回调函数会放到微任务队列里
  • 3、如果不设置回调函数, promise内部抛出的错误 不会反应到外部
  • 4、当处于pedding的状态时 无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

什么是promise规范​

我们在 JavaScript 中使用的 Promise 对象其实是 A+ 规范的实现,我们把这种规范称为 Promise/A+ 规范,但为了一般化,我们仍习惯于把它称为 Promise 规范。

Promise 规范有以下几个特点:

  • 对象的状态不受外界影响:Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
  • 只要状态改变一次,就不会再变,任何时候都可以得到这个结果:Promise 对象的状态改变,只有两种可能:从pending 变为fulfilled 和从pending 变为rejected。只要这两种情况之一发生了,那么它将会一直保持这个结果

你是如何用promise的?​

  • 接口的异步请求

  • 客户端数据来源多个数据,需要多个接口同时调用,可以用到promise

  • 解决异步编程

  • 解决回调地狱问题 (回调地狱代码难以维护,并且代码冗杂看起来难以理解)

    • 通过promise对象解决异步编程
    • 不会有嵌套的方式
    • 解决模块顺序执行问题
  • 可以用来封装一个图片异步加载的

Promise.then()​

then方法的第一个参数是 resolved状态的回调函数,第二个参数(可选)是 rejected状态的回调函数。

  • 因此只有当resolved被调用的时候,then才能接受返回的结果

Promise.all 和Promise.race的区别

相同点:接受的参数是可迭代的对象 【类似数组】

Promise.all :具有并发执行异步任务的能力

  • Promise.all可以将多个实例组装成一个新的实例,成功的时候返回一个成功数组,失败的时候则返回最先被reject失败状态的值
  • Promise.all只有等所有实例都成功状态,才会返回成功fulfilled的状态值
  • Promise.all()最大的缺陷就是只要有一个任务失败了,那所有的任务也都失败了

Promise.race :

  • race是赛跑的意思,也就是说Promise.race([p1, p2, p3])里面的结果哪个获取的快,就返回哪个结果,不管结果本身是成功还是失败

    • 简单来说,哪个promise实例的结果获取的快,就返回哪个结果,不管结果本身是成功还是失败
    • 一般用于和定时器绑定,比如将一个请求和一个三秒的定时器包装成Promise实例,加入到race队列中,请求三秒中还没有回应时,给用户一些提示或一些相应的操作。

Promise.allSettled()​

  • settled:意思是 稳定的、解决
  • allSettled方法出现就是来弥补Promise.all的方法

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled还是 rejected,包装实例才会结束。该方法由 ES2020 引入

Promise 的缺点​

  • Promise 固然是一个重大的进步,但在有些场景下仍然是不够的。比如,Promise 的特点是无论有没有人关心它的执行结果,它都会立即开始执行,并且你没有机会取消这次执行。显然,在某些情况下这么做是浪费的甚至错误的。仍然以电商为例,如果某商户的订单不允许取消,你还会去买吗?再举个编程领域的例子:如果你发起了一个 Ajax 请求,然后用户导航到了另一个路由,显然,你这个请求如果还没有完成就应该被取消,而不应该发出去。但是使用 Promise,你做不到,不是因为实现方面的原因,而是因为它在概念层(接口定义上)就无法支持取消。

无法取消promise执行

Promise.all、Promise.race、Promise.any的区别及场景​

参考:https://www.zhangxinxu.com/wordpress/2021/05/promise-all-race-any/

Promise.all: Promise序列会全部执行通过才认为是成功,否则认为是失败;

Promise.race: 哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

Promise.any: Promise序列只要有一个执行通过,则认为成功,如果全部失败,则认为失败;

Promise.rece()​

Promise.race接受多个Promise实例

Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态

使用场景:

  • 1、可实现并发请求数量控制
  • 2、处理请求超时的提示,发送请求的时候,如果接口超过一定的时间,比如超过3秒钟,还没有返回结果,就可以提示用户请求超时

请求超时处理例子:

js
// 第一个异步任务
function asyncOne() {
let async1 = new Promise(async (resolve, reject) => {
setTimeout(() => {
// 这里我们用定时器模拟后端发请求的返回的结果,毕竟都是异步的
let apiData1 = "某个请求";
resolve(apiData1);
}, 4000);
});
return async1;
}
console.log("异步任务一", asyncOne()); // 返回的是pending状态的Promise对象

// 第二个异步任务
function asyncTwo() {
let async2 = new Promise(async (resolve, reject) => {
setTimeout(() => {
let apiData2 = "超时提示";
resolve(apiData2);
}, 3000);
});
return async2;
}
console.log("异步任务二", asyncTwo()); // 返回的是pending状态的Promise对象

// Promise.race接收的参数也是数组,和Promise.all类似。只不过race方法得到的结果只有一个
// 就是谁跑的快,结果就使用谁的值
let paramsArr = [asyncOne(), asyncTwo()]

Promise
.race(paramsArr)
.then((value) => {
console.log("Promise.race方法的结果", value);
if (value == "超时提示") {
console.log('请求超时')
}else{
console.log('正常操作即可');
}
})

Promise.any()​

注意:使用的时候注意考虑兼容性问题,promise.any是后面出来的规范,有些低版本浏览器不兼容

使用场景:

  • 比如,有些资源在CDN上,不同的CDN加载的时间也不一样,可以两个资源都请求,哪个请求先成功就使用哪一个。
js
let startTime = +new Date();
let importUnpkg = import('https://unpkg.com/vue@3.0.11/dist/vue.runtime.esm-browser.js');
let importJsdelivr = import('https://cdn.jsdelivr.net/npm/vue@3.0.11/dist/vue.runtime.esm-browser.js');
Promise.any([importUnpkg, importJsdelivr]).then(vue => {
console.log('加载完毕,时间是:' + (+new Date() - startTime) + 'ms');
console.log(vue.version);
});

如何实现并发请求数量控制​

第三方库:

参考:

1、https://juejin.cn/post/7057026063225847821

2、https://zhuanlan.zhihu.com/p/455838344

方式1 【Promise.race】

  • 使用 Promise.race 判断是否已达到并发的最大数量。依次遍历数据,将异步请求放入并发池中,当并发池中的任务数量达到最大值时,通过 Promise.race 等待并发池中的任务完成,每完成一个任务,就将该任务从并发池中移除,然后将下一个数据的请求任务放入并发池中,直到所有任务都执行完成

  • 设置一个合理的超时值来限制请求的执行时间,并通过使用 Promise.race 方法来确定哪个请求先完成。

    为了防止请求无限期地等待响应,从而导致程序卡住。如果没有超时时间的限制,那么即使服务器无法响应,请求也会一直等待下去

    在超过超时时间后,如果还没有收到响应,就会触发超时错误。

核心步骤:

1、设置一个并发请求的数量的变量来控制

2、循环遍历url,将每一个url请求添加到数组中,每次循环都需要检查并发数是否已经达到了限制,如果达到了就调用就调用 Promise.race 方法来等待一个请求完成,然后移除已经完成的请求

3、最后,调用 Promise.all 方法来等待所有请求完成。

例子:

javascript
// 并发请求的数量
const CONCURRENCY = 3;

async function sendRequest(url) {
// 设置超时时间为 5 秒
const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('Request timed out')), 5000);
});

// 发起请求
const response = request(url);

// 返回最先完成的请求
return Promise.race([timeout, response]);
}

async function main() {
const urls = [
'http://example.com/1',
'http://example.com/2',
'http://example.com/3',
'http://example.com/4',
'http://example.com/5'
];

const promises = [];
for (const url of urls) {
promises.push(sendRequest(url));

// 如果当前的并发数已经达到了限制,就等待一个请求完成
if (promises.length >= CONCURRENCY) {
await Promise.race(promises);
// 移除已经完成的请求
promises.splice(0, 1);
}
}

// 等待所有请求完成
await Promise.all(promises);
}

简化版:

javascript
async function asyncPool(poolLimit, urls, iteratorFn) {
const ret = [];
const executing = [];
for (const item of urls) {
const p = Promise.resolve().then(() => iteratorFn(item));
ret.push(p);
if (poolLimit <= urls.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= poolLimit) {
await Promise.race(executing);
}
}
}
return Promise.all(ret);
}

// 使用
const timeout = ms => new Promise(resolve => setTimeout(() => resolve(ms), ms));

for await (const ms of asyncPool(2, [1000, 5000, 3000, 2000], timeout)) {
console.log(ms);
}

场景1 【控制多个图表渲染并发】​

参考:https://juejin.cn/post/7043701993189146654#heading-4

产品要求在界面上展示 1000个图表定时刷新 的公司最新的财务支出情况

无并发控制情况:

  • 直接循环1000个图表渲染,显然在浏览器运行直接会奔溃

    分析奔溃原因:

    • 大量数据量导致的性能问题,导致浏览器的性能下降
    • 大量的echart图表,如果没有及时销毁,可能会导致内存泄漏
    • 多个图表同时渲染,如果多个 echarts 图表同时渲染,可能会导致浏览器的负载过高,从而导致浏览器奔溃。可以尝试使用异步渲染

promise控制并发:

  • 必须保证一个图表渲染完成,再执行下一个渲染,此时我们就需要监听Echarts 的finished 事件

promise一旦执行就不能取消/中断,这样设计的目的是什么?​

  • 为了确保操作结果的一致性和可靠性。如果promise被取消,那么操作可能已经开始,但却没有完成,这将导致操作结果不确定,可能会产生不可预测的行为
  • promise对象是异步操作,异步操作通常包含涉及到多个步骤,这些步骤可能是相互依赖的,并且在执行过程中可能对外部环境造成了影响,如果在操作执行过程中取消了,这可能会给程序带来不可预测的后果
  • promise一旦执行就不能取消还有助于确保代码的可读性和可维护性。如果一个操作是可取消的,那么代码实现就必须考虑如何处理取消操作

确保异步操作的稳定性、正确性、可靠性

封装一个异步加载图片的方法​

  • 只需要在图片的onload函数中,使用resolve返回一下就可以了
js
function loadImg(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = function() {
console.log("一张图片加载完成");
resolve(img);
};
img.onerror = function() {
reject(new Error('Could not load image at' + url));
};
img.src = url;
});
。

手写promise的​

思路:

  • promise有状态pending、rejected和resolved,所以有个变量来保存状态
  • primise最重要的方法是then,需要实现then方法,需要接受2个回调函数分别是resolved和rejected​

js
class Promise {
//传一个异步函数进来
constructor(excutorCallBack) {
this.status = 'pending'
this.value = undefined
this.fulfillAry = [] // Promise resolve时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
this.rejectedAry = [] // Promise reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
//=>执行Excutor
let resolveFn = result => {
if (this.status !== 'pending') return
this.status = 'fulfilled'
this.value = result
this.fulfillAry.forEach(item => item(this.value))
}
let rejectFn = reason => {
if (this.status !== 'pending') return
this.status = 'rejected'
this.value = reason
this.rejectedAry.forEach(item => item(this.value))
}
try {
//执行这个异步函数
excutorCallBack(resolveFn, rejectFn)
} catch (err) {
//=>有异常信息按照rejected状态处理
rejectFn(err)
}
}
then(fulfilledCallBack, rejectedCallBack) {
//resolve和reject函数其实一个作为微任务
//因此他们不是立即执行,而是等then调用完成后执行
this.fulfillAry.push(fulfilledCallBack)
this.rejectedAry.push(rejectedCallBack)
//一顿push过后他们被执行
}
}
module.exports = Promise