一、Promise

1.1 Promise是什么?

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。 

1、主要用于异步编程
2、可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
3、可以在对象之间传递和操作promise,帮助我们处理队列 

1.2 为什么会有Promise?

1.2.1 为了避免界面冻结(任务)

  • 同步:假设你去了一家饭店,找个位置,叫来服务员,这个时候服务员对你说,对不起我是“同步”服务员,我要服务完这张桌子才能招呼你。那桌客人明明已经吃上了,你只是想要个菜单,这么小的动作,服务员却要你等到别人的一个大动作完成之后,才能再来招呼你,这个便是同步的问题:也就是“顺序交付的工作1234,必须按照1234的顺序完成”。

  • 异步:则是将耗时很长的A交付的工作交给系统之后,就去继续做B交付的工作,等到系统完成了前面的工作之后,再通过回调或者事件,继续做A剩下的工作。
    AB工作的完成顺序,和交付他们的时间顺序无关,所以叫“异步”。

1.2.2 异步操作的常见语法

1.事件监听

document.getElementById('#start').addEventListener('click', start, false);
function start() {
  // 响应事件,进行相应的操作
}
// jquery on 监听
$('#start').on('click', start)

2.回调

// 比较常见的有ajax
$.ajax('https://www.baidu.com/', {
 success (res) {
   // 这里可以监听res返回的数据做回调逻辑的处理
 }
})

// 或者在页面加载完毕后回调
$(function() {
 // 页面结构加载完成,做回调逻辑处理
})

有了nodeJS之后...对异步的依赖进一步加剧了

大家都知道在nodeJS出来之前PHP、Java、python等后台语言已经很成熟了,nodejs要想能够有自己的一片天,那就得拿出点自己的绝活:
无阻塞高并发,是nodeJS的招牌,要达到无阻塞高并发异步是其基本保障
举例:查询数据从数据库,PHP第一个任务查询数据,后面有了新任务,那么后面任务会被挂起排队;而nodeJS是第一个任务挂起交给数据库去跑,然后去接待第二个任务交给对应的系统组件去处理挂起,接着去接待第三个任务...那这样子的处理必然要依赖于异步操作

异步回调的问题:

  • 之前处理异步是通过纯粹的回调函数的形式进行处理
  • 很容易进入到回调地狱中,剥夺了函数 return 的能力
  • 问题可以解决,但是难以读懂,维护困难
  • 稍有不慎就会踏入回调地狱 - 嵌套层次深,不好维护

1.2.3 回调地狱

在正式了解“回调地狱”之前,我们先了解两个概念:

        1.回调函数
        当一个函数作为参数传入另一个函数中,并且它不会立即执行,只有当满足一定条件后该函数才可以执行,这种函数就称为回调函数。我们熟悉的定时器和Ajax中就存在有回调函数:

setTimeout(function(){   //function(){console.log('执行了回调函数')}就是回调函数,它只有在3秒后才会执行
	console.log('执行了回调函数');
},3000)  //3000毫秒

这里的回调函数function(){console.log('执行了回调函数')},在满足时间3秒后执行。

      //1.创建异步对象
			var xhr=new XMLHttpRequest();
      //2.打开链接(创建请求)
			xhr.open("get","/demo/ajaxDemo",true);
      //3.绑定监听事件(接收请求)
			xhr.onreadystatechange=function(){
				//此方法会被调用4次
				//最后一次,readyState==4
				//并且响应状态码为200时,才是我们要的响应结果 xhr.status==200
				if(xhr.readyState==4 && xhr.status==200){
					//把响应数据存储到变量result中
					var result=xhr.responseText;
					console.log(result);
				}
			}
      //4.发送请求
			xhr.send();

这里的回调函数是xhr.onreadystatechange绑定的函数,在xhr.send()发送请求并拿到响应后执行。

        2.异步任务
        与之相对应的概念是“同步任务”,同步任务在主线程上排队执行,只有前一个任务执行完毕,才能执行下一个任务。异步任务不进入主线程,而是进入异步队列/消息队列,前一个任务是否执行完毕不影响下一个任务的执行。同样,还拿定时器作为异步任务举例:

        setTimeout(function(){
            console.log('执行了回调函数');
        },3000)
        console.log('111');

如果按照代码编写的顺序,应该先输出“执行了回调函数”,再输出“111”。但实际输出为:

在这里插入图片描述

这种不阻塞后面任务执行的任务就叫做异步任务。
接下来让我们看看什么是回调地狱。 

1.2.3.1 回调地狱是什么?

 根据前面我们可以得出一个结论:存在异步任务的代码,不能保证能按照顺序执行,那如果我们非要代码顺序执行呢?

比如我要说一句话,语序必须是下面这样的:武林要以和为贵,要讲武德,不要搞窝里斗。
我必须要这样操作,才能保证顺序正确:

        setTimeout(function () {  //第一层
            console.log('武林要以和为贵');
            setTimeout(function () {  //第二程
                console.log('要讲武德');
                setTimeout(function () {   //第三层
                    console.log('不要搞窝里斗');
                }, 1000)
            }, 2000)
        }, 3000)

在这里插入图片描述

可以看到,代码中的回调函数套回调函数,居然套了3层,这种回调函数中嵌套回调函数的情况就叫做回调地狱

当异步操作想要有顺序时,只能在一个异步成功以后的回调函数里面嵌套另一个异步的操作,如果嵌套的层数过多就形成了回调地狱

回调地狱的弊端:后期代码维护比较困难

总结一下,回调地狱就是为是实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差,后期不好维护。

那该如何解决回调地狱呢?

1.3 如何创建promise对象

创建promise对象有两种方式:

方式一  通过Promise构造函数

  • promise是一个对象,对象和函数的区别就是对象可以保存状态,函数不可以(闭包除外)
  • 并未剥夺函数 return 的能力,因此无需层层传递callback,进行回调获取数据
  • 代码风格,容易理解,便于维护
  • 多个异步等待合并便于解决
new Promise(
  function (resolve, reject) {
    // 一段耗时的异步操作
    resolve('成功') // 数据处理完成
    // reject('失败') // 数据处理出错
  }
).then(
  (res) => {console.log(res)},  // 成功
  (err) => {console.log(err)} // 失败
)

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。 

  • resolve作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
    reject作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
  • promise对象的状态不受外界影响 (3种状态):
    1、pending[待定]初始状态
    2、fulfilled[实现]操作成功
    3、rejected[被否决]操作失败
    当promise状态发生改变,就会触发then()里的响应函数处理后续步骤;promise状态一经改变,不会再变。
  • 一旦状态改变就不会再变 (两种状态改变:成功或失败):
    从pending变为fulfilled(成功)
    从pending变为rejected(失败)
    这两种情况只要发生,状态就凝固了,不会再变了。

对于第一种方式,需要手动判断成功还是失败,比如你获取用户信息:

// 定义一个函数,用于获取用户信息
function getUserInfo(userId) {
  // 使用Promise手动管理异步操作
  return new Promise(function(resolve, reject) {
    // 假设获取用户信息是一个异步操作,需要一定时间
    setTimeout(function() {
      // 假设获取到了用户信息
      let user = { id: userId, name: '张三', age: 18 };
      // 判断用户是否存在
      if (user) {
        // 如果存在,使用resolve方法将Promise状态变为成功态
        resolve(user);
      } else {
        // 如果不存在,使用reject方法将Promise状态变为失败态
        reject('用户不存在');
      }
    }, 1000);
  });
}
 
// 调用getUserInfo函数获取用户信息
getUserInfo('001')
  .then(function(user) {
    // 如果获取用户信息成功,将打印用户信息
    console.log(user);
  })
  .catch(function(error) {
    // 如果获取用户信息失败,将打印错误信息
    console.log(error);
  });

在该例子中,使用Promise手动管理异步操作。在getUserInfo函数中创建了一个Promise对象,将异步操作封装在其中,当异步操作执行成功时,使用resolve方法将Promise状态变为成功态,并传递用户信息,当异步操作执行失败时,使用reject方法将Promise状态变为失败态,并传递错误信息。使用then方法和catch方法分别处理Promise的状态变化,如果Promise状态变为成功态,将打印用户信息,如果Promise状态变为失败态,将打印错误信息。

方式二  通过静态方法创建一个promise对象

也称为Promise的自动化管理方式。比如,Promise.resolve()可以创建一个状态为成功的Promise对象,Promise.reject()可以创建一个状态为失败的Promise对象。

// 第二种方式,自动管理
let promise = Promise.resolve('successful');
let promise = Promise.reject('failed');

 关于第二种方式,也给一个例子:

// 模拟一个异步操作函数
function asyncFunction() {
  return new Promise((resolve, reject) => {
    // 模拟一个异步操作,2秒钟后将结果返回
    setTimeout(() => {
      resolve('success');
    }, 2000);
  });
}
 
// 返回一个已解决的Promise对象,并使用函数返回值作为解决结果
Promise.resolve(asyncFunction())
  .then((value) => {
    console.log('异步操作执行成功', value);
    // 在这里处理异步操作执行成功的情况
  })
  .catch((error) => {
    console.error('异步操作执行失败', error);
    // 在这里处理异步操作执行失败的情况
  });

在这个例子中,我们定义了一个asyncFunction()函数,该函数返回一个promise对象,在promise对象的构造函数中使用setTimeout模拟了一个异步操作。然后我们使用Promise.resolve()方法将异步操作函数的返回值转换成一个自动管理状态的Promise对象。最后,我们在使用Promise.resolve()方法返回的Promise对象上使用.then()方法和.catch()方法处理异步操作成功或失败的情况。

使用Promise.resolve()方法的好处在于,如果被传入的参数本来就是一个promise对象,那么直接返回这个promise对象,如果不是promise对象,会自动转换成promise对象,方便在异步操作逻辑中使用。

无论是哪种方式,创建的promise对象都将具有pending(等待态)状态,调用resolve或reject方法可以改变其状态,并传递相应的值或错误。

最简单示例:

new Promise(resolve => {
  setTimeout(() => {
    resolve('hello')
  }, 2000)
}).then(res => {
  console.log(res)
})

分两次,顺序执行

    new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('hello')//  1参数val = 'hello'
        }, 1000)
    }).then(val => {
        console.log(val);
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('world')
            }, 1000)
        })
    }).then(val => {
        console.log(val);//  2参数val = 'world'
    })

promise完成后then()

    let pro = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('hello world')//  1参数val = 'hello'
        }, 1000)
    })
    setTimeout(() => {
        pro.then(val => {
            console.log(val); //  参数val = 'hello world'
        }, 1000)
    })
    console.log(pro.then(val => {}) instanceof Promise); // true

结论:promise作为队列最为重要的特性,我们在任何一个地方生成了一个promise队列之后,我们可以把他作为一个变量传递到其他地方。

.then()

1、接收两个函数作为参数,分别代表fulfilled(成功)和rejected(失败)
2、.then()返回一个新的Promise实例,所以它可以链式调用
3、当前面的Promise状态改变时,.then()根据其最终状态,选择特定的状态响应函数执行
4、状态响应函数可以返回新的promise,或其他值,不返回值也可以我们可以认为它返回了一个null;
5、如果返回新的promise,那么下一级.then()会在新的promise状态改变之后执行
6、如果返回其他任何值,则会立即执行下一级.then()

.then()里面有.then()的情况

1、因为.then()返回的还是Promise实例
2、会等里面的then()执行完,再执行外面的

 对于我们来说,此时最好将其展开,也是一样的结果,而且会更好读:

执行顺序

  接下来我们探究一下它的执行顺序,看以下代码:

let promise = new Promise(function(resolve, reject){
    console.log("AAA");
    resolve()
});
// promise.then    微任务
promise.then(() => console.log("BBB"));
console.log("CCC")

// AAA
// CCC
// BBB

  执行后,我们发现输出顺序总是 AAA -> CCC -> BBB。表明,在Promise新建后会立即执行,所以首先输出 AAA。然后,then方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以BBB 最后输出

与定时器混用

  首先看一个实例:

let promise = new Promise(function(resolve, reject){
    console.log("1");
    resolve();
});
setTimeout(()=>console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");

// 1
// 4
// 3
// 2

可以看到,结果输出顺序总是:1 -> 4 -> 3 -> 2。1与4的顺序不必再说,而2与3先输出Promise的then,而后输出定时器任务。原因则是promise.then属于微任务,而setTimeout则是宏任务,而任务优先级高于宏任务,所以有此结果。

错误处理

Promise会自动捕获内部异常,并交给rejected响应函数处理。

  1. 第一种错误处理 (throw new Error() 抛出错误)

2.第二种错误处理(reject() 抛出错误)

错误处理两种做法:
    第一种:reject('错误信息').then(() => {}, (error) => {错误处理逻辑})
    第二种:throw new Error('错误信息').catch( (error) => {错误处理逻辑})
    推荐使用第二种方式,更加清晰好读,并且可以捕获前面所有的错误(可以捕获N个then回调错误)

then函数的第二次回调和catch回调有什么区别?

当请求发生错误时,会触发 Promise 的 then 的第二个回调函数和 catch。 乍一看没有区别,但实际上前者无法捕获then当前第一个回调函数中抛出的错误,但catch可以。

Promise.resolve().then(
   () => {
     throw new Error('Error from success callback');
   },
   () => {
     // will not be executed
   }
).catch(reason => {
   console.log(reason.message); // Will print out "error from success callback"
});

 原理就如上一点所说的。 catch 函数是在 then 函数返回的处于拒绝状态的 Promise 上调用的,因此它的错误自然可以被捕获。

catch() + then()

第一种情况: then(() => { throw new Error("错误")})

结论:catch里抛出异常,catch会返回一个promise 实例,并且是 resolved 状态;会被后面catch捕获到,但不会终止会继续执行后面的then

第二种情况: catch(() => { throw new Error("错误")})

结论:catch里抛出异常,catch会返回一个promise 实例,并且是 rejected 状态,所以绕过两个then直接跑到最下面的catch

finally
finally()方法是Promise对象的原型方法,用于指定不论Promise对象状态如何都要被执行的回调函数,通常用来执行释放资源、清理操作等最终操作

finally()方法只有一个参数,就是要执行的回调函数。这个回调函数在Promise对象状态变为已解决(resolved)或已拒绝(rejected)时都会被执行,无论如何都会执行。以下是一个使用finally()方法的例子:

function asyncFunction() {
  return new Promise((resolve, reject) => {
    // 模拟一个异步操作,2秒钟后将结果返回
    setTimeout(() => {
      resolve('success');
    }, 2000);
  });
}
 
// 在异步操作结束后执行清理操作
asyncFunction()
  .then((value) => {
    console.log('异步操作成功', value);
    // 在这里对异步操作的结果进行处理
  })
  .catch((error) => {
    console.error('异步操作失败', error);
    // 处理异步操作的错误情况
  })
  .finally(() => {
    console.log('清理操作已执行');
    // 在这里执行清理操作,无论异步操作成功还是失败都会执行
  });

1.4 promise 常用静态方法

Promise.all([...]) 批量执行,该方法接受一个数组,返回一个 Promise 实例。 

Promise.all([p1, p2, p3])用于将多个promise实例,包装成一个新的promise实例,返回的实例就是普通的promise,它接收一个数组作为参数
数组里可以是Promise对象,也可以是别的值,只有Promise会等待状态改变
所有的子Promise都完成,该Promise完成,返回值是全部值的数组(所有成功的数据)
有任何一个失败,该Promise失败,返回值是第一个失败的子Promise结果

    //切菜
    function cutUp() {
        console.log('开始切菜。');
        var p = new Promise(function (resolve, reject) {        //做一些异步操作
            setTimeout(function () {
                console.log('切菜完毕!');
                resolve('切好的菜');
            }, 1000);
        });
        return p;
    }

    //烧水
    function boil() {
        console.log('开始烧水。');
        var p = new Promise(function (resolve, reject) {        //做一些异步操作
            setTimeout(function () {
                console.log('烧水完毕!');
                resolve('烧好的水');
            }, 1000);
        });
        return p;
    }

    Promise.all([cutUp(), boil()])
        .then((result) => {
            console.log('准备工作完毕');
            console.log(result); // ["切好的菜", "烧好的水"]
        })

//结果:
//reject
//Promise {<fulfilled>: undefined}

Promise.all()方法通常用于处理需要同时获取多个数据的情况,并将这些数据合并为一个结果返回。例如,在一个Web应用程序中,用户在提交订单时需要同时更新订单和用户信息,这时候就可以使用Promise.all()方法一次性向服务器发送两个请求,等待两个Promise都进入fulfilled状态后再进行下一步操作。

另外,Promise.all()方法也可以用于并行处理多个HTTP请求或I/O操作,以提高程序的执行效率。例如,我们从网站中获取多条数据,这些数据都需要通过HTTP请求来获取,在所有Promise进入fulfilled状态后,我们可以将这些数据进行合并,并对它们进行排序、过滤等操作。这样一来,我们就可以在不阻塞主线程的情况下,在较短的时间内获取到多个数据,提高了应用程序的响应速度和用户体验。

除了上述使用场景之外,Promise.all()方法还可以用于多个耗时操作的并行执行和等待,例如读取多个文件、并发执行多个函数等。总之,Promise.all()方法在项目中具有很多实际使用场景,可以帮助我们优化和改进代码的执行效率和用户体验。

Promise.allSettled([...]),该方法接受一个数组,返回一个 Promise 实例。

用来确定一组异步操作是否都结束了(不管成功或失败),接收所有的状态。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。 

当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束并返回reject状态。

用法示例:

var p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 4000, 'one');
});
var p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, 'two');
});
var p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, 'three');
});
var p4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, 'four');
});
var p5 = new Promise((resolve, reject) => {
  reject('reject');
});

Promise.allSettled([p1, p2, p3, p4, p5]).then(values => {
  console.log(values);
}, reason => {
  console.log(reason)
});
//结果:
//Promise {<pending>}
//(5) [{…}, {…}, {…}, {…}, {…}]
//0: {status: "fulfilled", value: "one"}
//1: {status: "fulfilled", value: "two"}
//2: {status: "fulfilled", value: "three"}
//3: {status: "fulfilled", value: "four"}
//4: {status: "rejected", reason: "reject"}
//length: 5
//[[Prototype]]: Array(0)

 一旦所指定的 promises 集合中每一个 promise 已经完成,无论是成功的达成或被拒绝,未决议的 Promise将被异步完成。那时,所返回的 promise 的处理器将传入一个数组作为输入,该数组包含原始 promises 集中每个 promise 的结果。

对于每个结果对象,都有一个 status 字符串。如果它的值为 fulfilled,则结果对象上存在一个 value 。如果值为 rejected,则存在一个 reason 。value(或 reason )反映了每个 promise 决议(或拒绝)的值。

可以发现和all相比,allSettled在其中一个promise返回错误时还可以继续等待结果。并且不管内部的计时器定时多少毫秒,它都会等所有结果返回后按照传参传入的顺序返回promise结果。

应用场景:
比如我们用户在页面上面同时填了3干个独立的表单,这三个表单分三个接口提交到后端,三个接口独立,没有顺序依赖,这个时候我们需要等到请求全部完成后给与用户提示表单提交的情况。

面试题:
共有四个接口,第一个接口是崩溃的,但是需要返回所有接口的结果。

这一题如果使用 all,那么会直接抛出错误,所以必须使用allSettled方法请求数据

 let a = axios.get("http://xxxa")
 let b = axios.get("http://xxxb")
 let c = axios.get("http://xxxc")
 let d = axios.get("http://xxxd")

 Promise.allSettled([a, b, c,d]).then(resx => {
   console.log('resx:',resx);
 },err => console.log(err))
/*结果:
第一个接口请求结果是错误的
(4) [{…}, {…}, {…}, {…}]
0: {status: "rejected", reason: Error:log…}
1: {status: "fulfilled", value: {…}}
2: {status: "fulfilled", value: {…}}
3: {status: "fulfilled", value: {…}}

*/
  • Promise.all 需要所有任务都返回成功才行,只有一个失败,就立即返回。比如一系列规则校验,全部通过才是校验成功,只要一个失败就立即返回失败。

  • Promise.allSettled 更关注一系列任务是否都完成(settled)了,同时无论任务成功(fulfilled)还是失败(rejected),每个任务的返回都能在最后的结果中查看。比如我上传三个文件,上传结果在所有任务都返回后再给出(如:上传成功两个,失败一个),同时能对失败的上传做额外处理。

 

Promise.race([...]),该方法同样接受一个数组,返回一个 Promise 实例。

Promise.race()方法同样是将多个Promise实例包装成一个新的Promise实例,但是只要有一个Promise 实例状态发生变化,就将新的Promise实例的状态改变,且终值由第一个完成的 Promise提供。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('promise1 resolved'), 2000);
});
const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('promise2 resolved'), 1000);
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('promise3 resolved'), 3000);
});
 
Promise.race([promise1, promise2, promise3]).then((value) => {
  console.log(value); // 'promise2 resolved'
});
 

Promise.race()方法也有许多实际使用场景。它可以用于处理需要快速获取结果的情况,例如,当我们向多个不同的服务器请求同一个资源时,我们可以使用Promise.race()方法来获取最快返回结果的服务器的响应,并忽略其他服务器的响应结果。或者,在一个Web应用程序中,我们需要在指定的时间内获取用户的同步输入和异步请求结果,我们可以使用Promise.race()方法同时监听用户输入事件和请求结果事件,一旦其中有一个事件触发,就可以立即返回响应结果,提高应用程序的响应速度和用户体验。

另外,Promise.race()方法还可以用于处理超时情况,例如,在一个HTTP请求的响应时间超过一定时间后,我们可以使用Promise.race()方法将该请求和一个延迟一定时间的Promise实例包装起来,一旦有一个Promise进入fulfilled状态,就可以立即返回响应结果。如果请求在规定的时间内仍未返回,则将其取消并返回一个错误信息给用户,以提高应用程序的可用性和稳定性。

Promise.any([...]),该方法同样接受一个数组,返回一个 Promise 实例。

  • 当传入的 promise 有一个变成成功状态(fulfilled),Promise.any 返回的 promise 也会随着变成完成状态(fulfilled),返回的内容是最先 resolve 的 promise 的返回。

  • 当所有传入的 promise 变成了拒绝状态(rejected),Promise.any 返回的 promise 也会变成拒绝状态(rejected),返回的内容是个对象,对象的 errors 属性对应传入的各个 promise 的返回。

Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

const promises = [
  Promise.reject(1),
  Promise.reject(2),
  Promise.resolve(3),
];
 
Promise.any(promises)
  .then((value) => console.log('resolve',value))  // 3
  .catch((errors) => console.log('reject',errors)); 

如果所有的promise都是reject的,就抛出异常:

const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'quick'));
const promise3 = new Promise((resolve, reject) => setTimeout(reject, 500, 'slow'));

const promises = [promise1, promise2, promise3];

Promise.any(promises).catch((value) => {
    console.dir(value.errors)
});
// 0.5s 后输出:[0, 'quick', 'slow']

Promise.any()方法可以用于处理多种资源竞争的情况,例如,在一个抢单系统中,多个用户需要争夺同一个订单,系统将同时向多个用户发送请求,并使用Promise.any()方法监听所有请求的状态,一旦有一个用户成功抢到订单,系统就立即返回订单信息并发送通知给该用户,从而提高了用户的参与度和系统的可用性。

除此之外,Promise.any()方法还可以用于指定默认值或备选方案,例如,在一个多语言网站中,我们需要从多个API获取多语言翻译结果,但有些API可能由于网络原因或其他问题无法正常工作,这时候我们就可以使用Promise.any()方法来一次性向多个API发送请求,并设置一个默认值或备选方案,一旦有一个API正常返回翻译结果,就立即返回结果给用户,如果所有API都无法正常工作,则返回默认值或备选方案。

  • Promise.race 更关注第一个返回的,无论状态是成功还是失败。

  • Promise.any 则是关注第一个成功返回的

 

Promise.withResolvers(),返回一个包含新的 Promise 对象和两个函数的对象,用于解决或拒绝该 Promise。这两个函数对应于传递给 Promise() 构造函数的执行器的两个参数。

当我们需要把回调函数式的异步编程兼容成 Promise 时,我们通常会用 new Promise((resolve, rejcet) => {}) 来处理,有了 Promise.withResolvers 后,代码会变得更简单。

const { promise, resolve, reject } = Promise.withResolvers()

// 等同于
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

 异步编程可以根据自己喜欢选择编写方式:

const { promise, resolve, reject } = Promise.withResolvers()
setTimeout(resolve, 1000, 'done')
promise.then(res => console.log(res))
// 1s 后输出: done

// 等同于
const promise = new Promise((resolve, reject) => {
    setTimeout(resolve, 1000, 'done')
})
promise.then(console.log)

二、Promise是没有中断方法的,当需要在链式中中断请求的时候要怎么处理?

讲道理,我们回忆下就知道 Promise 的特性就是:不能中断。

一旦执行,我们无法知道它具体执行到哪里了,只知道在 pending,最后 resolve 或者 reject 才知道执行完毕。

promise可以通过在流程中使用 throw 来抛出异常中断流程触发catch操作,也可以在某一个节点进行 reject来中断操作它的链式调用的.then函数 并不代表每一次的对象都是原始的promise对象。所以在链式调用的过程中是完全可以实现中断操作的。

方式一:同步的中断 promise 

function someAsyncFunction() {
  return new Promise(function(resolve, reject) {
    // 在这里执行异步操作
    if (/* 某个条件成立 */) {
      // 如果条件成立,中断 promise 
      // throw new Error("The promise was interrupted")
      reject(new Error("The promise was interrupted"));
    }
  });
}

someAsyncFunction().catch(function(error) {
  // 处理 promise 中断的回调函数
  console.error(error.message);
});

方式二:用 Promise 封装异步请求,当超过 N 秒后还未执行完,中断 Promise ,执行后续操作;

function timeoutWrapper(p, timeout = 2000) {
  const wait = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('请求超时')
    }, timeout)
  })
  return Promise.race([p, wait])
}

catch(() => { throw new Error("错误")}) 

三、拓展 async/await     同步代码异步操作,两者必须搭配使用

3.1 async

首先我们看async关键字,他作为一个关键字放到声明函数前面,表示该函数为一个异步任务,不会阻塞后面函数的执行。

  顾名思义,异步。async函数对 Generator 函数的改进,async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。特点体现在以下四点:

  • 内置执行器
  • 更好的语义
  • 更广的适用性
  • 返回值是 Promise

1)表明程序里可能有异步过程

async表明程序里可能有异步过程(有await关键字);全部是同步代码也没关系,但async就显得多余了;

2)非阻塞

async函数里如果有异步过程会等待,但async函数本身会马上返回,不会阻塞当前线程。可以简单认为async函数工作在主线程,同步执行,不会阻塞界面渲染;async函数内部由await修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回;

3)async函数返回类型为Promise对象

async函数会返回一个promise,并且Promise对象的状态值是resolved(内未有await)

如果没有在async函数中写return,那么Promise对象resolve的值就是是undefined

如果写了return,那么return的值就会作为成功的时候传入的值

这是和普通函数本质上不同的地方,也是使用时重点注意的地方:

(1)return new Promise();这个符合async函数本意;

(2)return data;这是同步函数的写法,特别注意这其实就相当于Promise.resolve(data);还是一个Promise对象;但在调用async函数的地方通过简单的=是拿不到这个data的,因为返回值是一个Promise对象,所以需要用.then(data => { })才可以拿到data

(3)如果没有返回值,相当于返回了Promise.resolve(undefined);

const fn1 = async () => 1;
const fn2 = () => Promise.resolve(1);

fn1(); // Also returns a promise object with a value of 1

 4)无等待

在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且不会阻塞后面的语句,这和普通返回Promise对象的函数并无二致;

5)内部的await不处理异步error

await是不管异步过程的reject(error)消息的,async函数返回的这个Promise对象的catch函数负责统一抓取内部所有异步过程的错误;async函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise对象的catch就能抓取到这个错误;

3.2 await

  顾名思义,等待。正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。await后的Promise如果是reject状态, 那么整个async函数都会中断,后面的代码不执行。

混合使用

  先看示例:

function sleep(ms) {
    return new Promise(function(resolve, reject) {
        console.log("CCC");
        resolve("promise1")
        setTimeout(function() {
            console.log("DDD");
            
        },ms);
    })
}
async function handle(){
    console.log("AAA")
    await sleep(2000).then((res) => {
        console.log(res)
    });
    console.log("BBB")
}

handle()

// AAA
// CCC
// promise1
// BBB 
// DDD

我们定义函数sleep,返回一个Promise。然后在handle函数前加上async关键词,这样就定义了一个async函数。在该函数中,利用await来等待一个Promise。

为什么叫await等待呢,因为当代码执行到async函数中的await时,代码就在此处等待不继续往下执行,知道await拿到Promise对象中resolve的数据,才继续往下执行,这样就保证了代码的执行顺序,而且使异步代码看起来更像同步代码。如果是reject状态, 那么整个async函数都会中断,后面的代码不执行。

Promise优缺点

为什么async/await更好?

  • 使用async函数可以让代码简洁很多
  •  不需要像Promise一样需要then
  • 不需要写匿名函数处理Promise的resolve的值 
  • 也不需要定义多余的data变量,还避免了嵌套代码 

总结
总结一下,当我们写代码遇到异步回调时,我们想让异步代码按照我们想要的顺序执行,如果按照传统的嵌套方式,就会出现回调地狱,这样的代码不利于维护,我们可以通过Promise对象进行链式编程来解决,这样尽管可以解决问题,但是ES7给我们提供了更加舒适的async/await语法糖,可以使得异步代码看起来更像是同步代码。
 

实战示例,回调地狱和promise对比:

/***
   第一步:找到北京的id
   第二步:根据北京的id -> 找到北京公司的id
   第三步:根据北京公司的id -> 找到北京公司的详情
   目的:模拟链式调用、回调地狱
 ***/
 
 // 回调地狱
 // 请求第一个API: 地址在北京的公司的id
 $.ajax({
   url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/city',
   success (resCity) {
     let findCityId = resCity.filter(item => {
       if (item.id == 'c1') {
         return item
       }
     })[0].id
     
     $.ajax({
       //  请求第二个API: 根据上一个返回的在北京公司的id “findCityId”,找到北京公司的第一家公司的id
       url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/position-list',
       success (resPosition) {
         let findPostionId = resPosition.filter(item => {
           if(item.cityId == findCityId) {
             return item
           }
         })[0].id
         // 请求第三个API: 根据上一个API的id(findPostionId)找到具体公司,然后返回公司详情
         $.ajax({
           url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/company',
           success (resCom) {
             let comInfo = resCom.filter(item => {
               if (findPostionId == item.id) {
                 return item
               }
             })[0]
             console.log(comInfo)
           }
         })
       }
     })
   }
 })
// Promise 写法
  // 第一步:获取城市列表
  const cityList = new Promise((resolve, reject) => {
    $.ajax({
      url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/city',
      success (res) {
        resolve(res)
      }
    })
  })

  // 第二步:找到城市是北京的id
    cityList.then(res => {
      let findCityId = res.filter(item => {
        if (item.id == 'c1') {
          return item
        }
      })[0].id
      
      findCompanyId().then(res => {
        // 第三步(2):根据北京的id -> 找到北京公司的id
        let findPostionId = res.filter(item => {
            if(item.cityId == findCityId) {
              return item
            }
        })[0].id

        // 第四步(2):传入公司的id
        companyInfo(findPostionId)

      })

    })

  // 第三步(1):根据北京的id -> 找到北京公司的id
  function findCompanyId () {
    let aaa = new Promise((resolve, reject) => {
      $.ajax({
        url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/position-list',
        success (res) {
          resolve(res)
        }
      })
    })
    return aaa
  }

// 第四步:根据上一个API的id(findPostionId)找到具体公司,然后返回公司详情
function companyInfo (id) {
  let companyList = new Promise((resolve, reject) => {
    $.ajax({
      url: 'https://www.easy-mock.com/mock/5a52256ad408383e0e3868d7/lagou/company',
      success (res) {
        let comInfo = res.filter(item => {
            if (id == item.id) {
               return item
            }
        })[0]
        console.log(comInfo)
      }
    })
  })
}

四、怎么解决promise的回调地狱

"回调地狱"通常指的是多层嵌套的回调函数,也称为"promise地狱",因为这会导致代码难以阅读和维护。为了解决这个问题,可以使用以下方法:

1. 使用async/await(如果环境支持ES6及以上版本)

async/await提供了一种更为清晰的方式来编写异步代码,它使用try/catch来处理异步操作的错误,并且可以避免使用回调。

解决方案示例代码:

async function fetchData() {
    try {
        const result1 = await firstAsyncCall();
        const result2 = await secondAsyncCall(result1);
        const result3 = await thirdAsyncCall(result2);
        // ... 更多的异步操作
    } catch (error) {
        // 错误处理
    }
}

2. 使用Promise.then()链而不是嵌套。

将第二个then作为第一个then返回的Promise的一部分,这样可以避免嵌套。

解决方案示例代码:

firstAsyncCall()
    .then(result1 => {
        return secondAsyncCall(result1);
    })
    .then(result2 => {
        return thirdAsyncCall(result2);
    })
    .then(result3 => {
        // ... 更多的操作
    })
    .catch(error => {
        // 错误处理
    });

3. 使用Promise.all()来并行执行多个Promise

如果多个Promise之间没有依赖关系,可以使用Promise.all()并行执行它们。

解决方案示例代码:

Promise.all([firstAsyncCall(), secondAsyncCall(), thirdAsyncCall()])
    .then(results => {
        // 使用结果数组处理
    })
    .catch(error => {
        // 错误处理
    });

4. 使用bluebird或其他库的Promise.coroutine来避免回调地狱。

这些工具可以帮助你用类似于async/await的方式写同步代码风格的异步代码。

总结,要解决"promise的回调地狱",可以使用async/awaitPromise.then()链,或者Promise.all()来并行执行多个Promise。如果环境支持,优先考虑使用async/await,因为它更直观,易读,并且提供了错误处理机制。

五、Promise 高级用途的技巧

1. Promise数组的串行执行

例如,如果你有一组接口需要串行执行,你可能首先想到使用await。


const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
for (const requestItem of requestAry) {
  await requestItem();
}

 如果使用promise,可以使用then函数串联多个promise,实现串行执行。


const requestAry = [() => api.request1(), () => api.request2(), () => api.request3()];
const finallyPromise = requestAry.reduce(
     (currentPromise, nextRequest) => currentPromise.then(() => nextRequest()),
     Promise.resolve() // Create an initial promise for linking promises in the array
);

2. 承诺实施请求共享

当一个请求已经发出但尚未得到响应时,再次发出相同的请求,就会造成请求的浪费。 此时,我们可以将第一个请求的响应与第二个请求共享

request('GET', '/test-api').then(response1 => {
  // ...
});
request('GET', '/test-api').then(response2 => {
  // ...
});

上述两个请求实际上只发送一次,同时收到相同的响应值。

那么,请求共享有哪些使用场景呢? 我认为有以下三个:

  • 当页面渲染多个内部组件同时获取数据时;

  • 提交按钮未禁用且用户连续多次点击提交按钮;

  • 预加载数据的情况下,预加载完成之前进入预加载页面;

这也是alova的高级功能之一。 要实现请求共享,需要使用promise的缓存功能,即一个promise对象可以通过多次await获取数据。 简单的实现思路如下:


const pendingPromises = {};
function request(type, url, data) {
   // Use the request information as the only request key to cache the promise object being requested
   //Requests with the same key will reuse promise
   const requestKey = JSON.stringify([type, url, data]);
   if (pendingPromises[requestKey]) {
     return pendingPromises[requestKey];
   }
   const fetchPromise = fetch(url, {
     method: type,
     data: JSON.stringify(data)
   })
   .then(response => response.json())
   .finally(() => {
     delete pendingPromises[requestKey];
   });
   return pendingPromises[requestKey] = fetchPromise;
}

 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐