出现背景

CodeWhy:新技术的出现,都是为了解决原有的某个痛点
先来看几个问题

例子1

  1. function random () {
  2. setTimeout(() => { // 这个是setTimeout 里面有一个箭头函数
  3. return parseInt(Math.random() * 6) + 1
  4. }, 1000)
  5. // return undefined
  6. }
  7. console.log(random());
  • random() 这个函数有 return 吗?
    • 答 : 没有写return , 只会 return undefined
  • setTimeout 里面有一个箭头函数,有 return 吗?
    • 答 : 有写return , 返回真正的值
    • 所以setTimeout 里面的箭头函数是 异步函数/任务
  • 请分清random() 这个函数 和 setTimeout 里面的箭头函数
    • 里面的两个 return 是不同的

      例子2

      ```javascript const n = random();

console.log(n) // undefined

  1. - 为什么console.log(n) 得到undefined ?
  2. - 因为random() 没有 return
  3. - console.log(n)要立刻得到结果
  4. - 所以得到 undefined
  5. - 正确写法
  6. - 如何拿到 setTimeout 里面的箭头函数里面的异步结果 ?
  7. - 使用回调
  8. ```javascript
  9. function random (fn){
  10. setTimeout(()=>{
  11. fn(parseInt(Math.random() * 6))
  12. },1000)
  13. }
  14. //这个是一个回调 写个函数,然后把函数地址给它
  15. function fn1 (x){
  16. console.log(x)
  17. }
  18. //接收这个函数
  19. random(fn1)
  20. // 然后在求随机数函数得到结果后 把结果作为参数传给fn1
  21. //内部逻辑
  22. //相当于
  23. function (fn1){
  24. setTimeout(()=>{
  25. fn1(parseInt(Math.random() * 6)) //结果变成参数 传给 fn1
  26. },1000) //fn1 得到参数 => 打印
  27. }
  • 随机函数得到 结果后把 结果 作为 参数 传给fn1

    • 这个就可以得到异步结果

      简化

  • 写法简化

    • 由于fn1声明之后只用了一次,所以可以删掉 fn1 ```javascript function fn1(x){ console.log(x) } random(fn1)

//简化为 random( x => console.log(x) )

//再次简化 => 只有一个参数的时候才可以用 random( console.log )

  1. - 注意
  2. - 如果参数个数不一致就不能这样简化
  3. <a name="pWCHW"></a>
  4. #### 关于简化有一个面试题
  5. ![](https://cdn.nlark.com/yuque/0/2020/png/1255355/1608511202654-eab15c00-a9ed-47bb-81cd-efebe03a1b7c.png#crop=0&crop=0&crop=1&crop=1&from=url&id=jJ0CH&margin=%5Bobject%20Object%5D&originHeight=198&originWidth=1038&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
  6. - 这个简化了为什么打印出 NaN ?
  7. 1. 这个代码正确写法
  8. ```javascript
  9. let array = ['1', '2', '3'].map((item,i,arr) => {
  10. return parseInt(item)
  11. })
  12. console.log(array); //[1,2,3]
  1. 像面试题里面简写就相当于这样写
    1. let array = ['1', '2', '3'].map((item, i, arr) => {
    2. return parseInt(item)
    3. // parseInt('1', 0, arr) => 1
    4. // parseInt('2', 1, arr) => NaN
    5. // parseInt('3', 1, arr) => NaN
    6. })
    7. console.log(array);
    parseInt(string, radix)
参数 描述
string 必需 要被解析的字符串。
radix 可选 表示要解析的数字的基数。该值介于 2 ~ 36 之间。
如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

总结

  • 异步任务不能拿到结果
  • 于是我们传一个回调给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

    Promise

  • 如果异步任务有两个结果,成功或失败,我们改如何做?

    Promise出现之前处理异步函数

    1、回调函数

    在异步操作执行完后,执行一个函数告诉我们执行完了

    1. function double(value, callback) { // 这里callback就是回调函数,什么名字都可以
    2. setTimeout(() => callback(value * 2), 1000);
    3. }
    4. double(3, (x) => console.log(`I was given: ${x}`));
    5. // I was given: 6(大约 1000 毫秒之后)

    2、失败处理

    成功回调和失败回调 ```javascript function double(value, success, failure) { setTimeout(() => { try { if (typeof value !== ‘number’) {

    1. throw 'Must provide number as first argument';

    } // 成功时执行 success(2 * value); } catch (e) { // 失败时执行 failure(e); } }, 1000); }

// 定义函数执行成功时,回调函数 const successCallback = (x) => console.log(Success: ${x});

// 定义函数执行失败时,回调函数 const failureCallback = (e) => console.log(Failure: ${e});

double(3, successCallback, failureCallback); double(‘b’, successCallback, failureCallback);

// Success: 6(大约 1000 毫秒之后) // Failure: Must provide number as first argument(大约 1000 毫秒之后)

  1. <a name="oJ1DW"></a>
  2. #### 3、嵌套回调
  3. 随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。<br />嵌套回调的代码维护起来就是噩梦<br />这三个问题也是上面出现背景中提到的问题
  4. <a name="mty2r"></a>
  5. ### Promise(期约)
  6. <a name="OCbXY"></a>
  7. #### 是什么?
  8. - Promise是一个类,可以翻译成承诺、许诺、期约;
  9. - 当我们需要给予调用者一个承诺∶待会儿我会给你回调数据时,就可以创建一个Promise的对象;
  10. - 在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor;
  11. - 这个回调函数会被立即执行,并且给传入另外两个回调函数resolve、reject ;
  12. - 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
  13. - 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数;
  14. <a name="f9LcT"></a>
  15. #### 应用场景
  16. 一般情况下是有异步操作时,使用 Promse对这个异步操作进行封装
  17. <a name="ZyX5h"></a>
  18. ### 基本使用
  19. ```javascript
  20. let a = new Promise((resolve,reject) =>{ // 通常命名为resolve解决,reject拒绝
  21. // 这里的函数会立刻执行,
  22. // 如果在函数里面执行了resolve(),后面就会执行.then 方法,不执行.catch 方法
  23. // 如果在函数里面执行了reject(),后面就会跳过.then 方法,直接执行.catch 方法
  24. // 例子这里放网络请求的代码
  25. let res = request() // 比如这是个例子
  26. if(res == true){ // 这里验证只是举个例子,实际要看返回的结果来作判断
  27. //请求成功的话,请求的结果通过resolve函数传到then里面
  28. resolve("abc")
  29. }else{
  30. //失败的话,把失败信息传到reject函数里面
  31. reject("fail")
  32. }
  33. }).then(data =>{
  34. //这里可以处理resolve传过来的数据
  35. console.log(data) // 这里例子就是上面resolve("abc")里面的"abc"
  36. }).catch(err =>{
  37. //这里可以获得reject传过来的错误信息,然后处理
  38. console.log(err) // 这里例子就是上面reject("fail")里面的"fail"
  39. })

Promise 的状态

Promise 是一个有状态的对象,可能处于如下 3 种状态之一:

1、待定(pending)

最初始状态,如果 Promise(()=>{ })里面的函数体,没有调用 resolve( ) 完成,或者没有调用reject( ) 失败,则会一直保持待定状态
只要出了结果,过程是不可逆的,无法重复修改结果。
Promise - 图1

2、兑现(fulfilled)

有时候也称为“解决”,resolved,表示已经成功完成
Promise - 图2

3、拒绝(rejected)

表示没有成功完成,此时会提示一定要有个catch方法捕获这个失败的信息
Promise - 图3
比如,假设Promise要向服务器发送一个 HTTP 请求。
请求返回 200~299 范围内的状态码就足以让Promise的状态变为“resolved”。
类似地,如果请求返回的状态码不在 200~299 这个范围内,那么就会把期约状态切换为“rejected”。

resolve 成功的处理

情况一

如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;
Promise - 图4

情况二

如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态︰
Promise - 图5

情况三

如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据then方法的结果来决定Promise的状态
Promise - 图6

then 方法

说明

then方法是Promise对象上的一个方法∶它其实是放在Promise的原型上的Promise.prototype.then
对象的方法:首先要new 一个对象,然后执行这个对象.方法()
Promise - 图7

方法参数

then方法接受两个参数∶

  • fulfilled(resolve)的回调函数︰当状态变成fulfilled(resolve)时会回调的函数;
  • reject的回调函数︰当状态变成reject时会回调的函数;

Promise - 图8

多次调用

image.png
image.png image.png (多次调用,共用同一个结果res)

返回值

1、普通值

如果返回值是基本数据类型、对象、数组、undefined(没有return返回)等,会当做新的Promise的resolve的值
image.png 相当于 image.png

2、new Promise

如果返回一个new Promise,则后面的状态(.then .catch),由这个返回的Promise里面的状态决定
image.png

3、对象且实现then方法

还是会包一层new Promise 然后返回
image.png
相当于
image.png

链式调用

原理就是内部会包一层 new Promise 然后返回,下面的就会接收上面的返回值,进行调用(见上面 返回值)
image.png

catch 方法(错误处理)

说明

catch方法也是Promise对象上的一个方法∶它也是放在Promise的原型上的Promise.prototype.catch
对象的方法:首先要new 一个对象,然后执行这个对象.方法()
Promise - 图18

捕获错误

除了Promise里面调用reject方法会执行catch,抛出错误(throw)也会触发catch
Promise - 图19

  • 这里相当于包装了一层new Promise 然后 return,详见then方法里面的返回值章节
  • then( ) 的第二个参数就是catch( )方法
  • 运行结果:

image.png

多次调用

和then一样,可以同一个对象多次调用,这样会以同一个结果来执行多个catch方法

  1. let promise = new Promise((resolve, reject) => {
  2. reject('aaa')
  3. });
  4. promise.catch(err => {
  5. console.log('err:', err);
  6. })
  7. promise.catch(err => {
  8. console.log('err:', err);
  9. })
  10. //err: aaa
  11. //err: aaa

链式调用

参考 then 方法里面的链式调用,只要在链式调用的过程中,出现了 reject 或者 出现了异常new Error 或者 代码报错,则会跳过下面所有的.then 方法,直接执行.catch( ) 方法(或者某个then() 方法的第二个参数,也是catch方法)
image.png

返回值

catch 也有返回值,也是相当于包装了一层 new Promise,然后return出去,因此还可以在后面接then 和 catch
image.png

finally 最终执行(ES9)

  • finally是在ES9(ES2018 )中新增的一个特性∶表示无论Promise对象无论变成fulfilled还是reject状态,最终都会被执行的代码。
  • finally方法是不接收参数的,因为无论前面是fulfilled状态,还是reject状态,它都会执行。

image.png image.png

Promise 的类方法

上面介绍的then、catch、finally方法都属于Promise的实例方法,都是存放在Promise的prototype上的。下面总结一下Promise的类方法。
通过 Promise . 类方法( ) 的方式调用

resolve( ) 直接成功

  1. // 类方法Promise.resolve()
  2. let promise = Promise.resolve({ name: '张三' });
  3. // 相当于
  4. let promise2 = new Promise((resolve, reject) => {
  5. resolve({ name: '张三' })
  6. })

直接使用 Promise 这个类的方法,可以简化 Promise 的编写。
它的参数和 new Promise 里面resolve的是一样的。见上《resolve 成功的处理》。

reject( ) 直接失败

  • reject方法类似于resolve方法,只是会将Promise对象的状态设置为reject状态。
  • Promise.reject的用法相当于new Promise,只是会调用reject
  • Promise.reject传入的参数无论是什么形态,都会直接作为reject状态的参数传递到catch的。

    1. // 类方法Promise.reject()
    2. Promise.reject('张三');
    3. // 相当于
    4. new Promise((resolve, reject) => { reject('张三') })

    直接使用 Promise 这个类的方法,可以简化 Promise 的编写。

    all( ) 等到一起完成

    它的作用是将多个Promise包裹在一起形成一个新的Promise ;

    1. let p1 = new Promise((resolve, reject) => {
    2. resolve('111');
    3. })
    4. let p2 = new Promise((resolve, reject) => {
    5. resolve('222');
    6. })
    7. let p3 = new Promise((resolve, reject) => {
    8. resolve('333');
    9. })

    Promise会等到 p1、p2、p3都完成(只要不是待定pending状态,就是成功或失败)后,再执行后面then 或 catch的内容

    1. Promise.all([p1, p2, p3]).then(res => {
    2. console.log(res);//['111', '222', '333']
    3. })
  • 新的Promise状态由包裹的所有Promise共同决定︰

    • 当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,并且会将所有Promise的返回值组成一个数组;
    • 当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数;

类似与&&操作,所有都成功,返回的就是成功;只要有一个失败,就会立刻返回的就是失败,其他没完成的就不执行了。

  • 成功的话,会把所有返回的结果,放在数组里面
  • 失败的话,返回的结果只有那个失败的结果

    allSettled( ) 等到一起完成(ES11)

  • all方法有一个缺陷︰当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。

    • 那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;
  • 在ES11 (ES2020 )中,添加了新的API Promise.allSettled :

    • 该方法会在所有的Promise都有结果( settled ),无论是fulfilled,还是reject时,才会有最终的状态
    • 并且这个Promise的结果一定是fulfilled的;
      1. let p1 = new Promise((resolve, reject) => {
      2. resolve('111');
      3. })
      4. let p2 = new Promise((resolve, reject) => {
      5. resolve('222');
      6. })
      7. let p3 = new Promise((resolve, reject) => {
      8. resolve('333');
      9. })
      10. Promise.allSettled([p1, p2, p3]).then(res => {
      11. console.log(res);
      最终返回的是一个数组,里面按数组的顺序陈列结果和传递的值
      image.png

      race( ) 第一个完成的

      race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果;
      1. let p1 = new Promise((resolve, reject) => {
      2. resolve('111');
      3. })
      4. let p2 = new Promise((resolve, reject) => {
      5. resolve('222');
      6. })
      7. let p3 = new Promise((resolve, reject) => {
      8. resolve('333');
      9. })
      10. Promise.race([p1, p2, p3]).then(res => {
      11. console.log(res);
      12. }).catch(err => {
      13. console.log(err);
      14. })
      谁先完成就用谁的结果,这个结果是成功的,就调用then;这个结果是失败的,就调用catch

      any( ) 任意成功的 (ES12)

  • any方法是ES12中新增的方法,和race方法是类似的:

    • any方法会等到一个fulfilled状态,才会决定新Promise的状态;
    • 如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态;

Promise - 图26
如果所有的Promise都是reject的,那么会报一个AggregateError的错误。
然后执行catch,把这个错误传入catch;
你还可以通过err.errors 这个属性获取上面所有Promise 返回的错误值
image.png

其他特性

1、比常规代码推后执行*

  1. let p1 = Promise.resolve();
  2. p1.then(() => console.log('5'));
  3. console.log('1');
  4. let p2 = Promise.reject();
  5. p2.then(null, () => console.log('6'));
  6. console.log('2');
  7. let p3 = Promise.reject();
  8. p3.catch(() => console.log('7'));
  9. console.log('3');
  10. let p4 = Promise.resolve();
  11. p4.finally(() => console.log('8'));
  12. console.log('4');
  13. // 输出为
  14. // 1 2 3 4 5 6 7 8

Promise 的代码,会比一般常规的代码,退后执行(如上例子),是因为 JavaScript 的事件循环机制。

1、首先,代码由上往下,一条一条执行,遇到一般的代码,比如创建变量、执行普通函数等,都会在左边的 内存里面的 堆/栈 结构里面执行。

2、遇到Promise、setTimeout等,就会把这些代码放进下方的回调队列里面排队,等一般代码执行完后,再执行这里队列的。

因此如上例子,才会出现,先输出1234(一般代码,堆/栈里面执行),再输出5678(进队列里面,后面再执行)
image.png

2、执行顺序

如果给Promise添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行

  1. let p1 = Promise.resolve();
  2. let p2 = Promise.reject();
  3. p1.then(() => setTimeout(console.log, 0, 1));
  4. p1.then(() => setTimeout(console.log, 0, 2));
  5. // 1
  6. // 2
  7. p2.then(null, () => setTimeout(console.log, 0, 3));
  8. p2.then(null, () => setTimeout(console.log, 0, 4));
  9. // 3
  10. // 4
  11. p2.catch(() => setTimeout(console.log, 0, 5));
  12. p2.catch(() => setTimeout(console.log, 0, 6));
  13. // 5
  14. // 6
  15. p1.finally(() => setTimeout(console.log, 0, 7));
  16. p1.finally(() => setTimeout(console.log, 0, 8));
  17. // 7
  18. // 8

3、超时失败

设定一个时间,超过时间还没完成就返回拒绝。
因为Promise的状态只能改变一次,所以超时拒绝逻辑中可以放心地设置让Promise处于待定状态的最长时间。
如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败。

  1. let p = new Promise((resolve, reject) => {
  2. // 先设置超时时间,如 10 秒后调用 reject()
  3. setTimeout(reject, 10000);
  4. // 再执行函数的逻辑
  5. // ...
  6. });