跟着coderwhy学习JavaScript高级(十九)

583 阅读3分钟

异步请求的处理方式

回调函数

所谓的回调函数就是函数作为参数的传递,在一个函数内部调用另一个函数,调用的同时可以把内部函数的数据传递出来,他的使用场景就是异步操作,数据需要等待一段时间才能返回的情况下可以使用回调函数。

    function foo(url, successCallback, failtureCallback) {
      // 假设这是网络请求,需要请求完毕才能返回给用户使用
      setTimeout(() => {
        if (url === 'success') {
         // 假设successCallback callback === 'function'
          successCallback && successCallback({ code: '200', message: '请求成功' })
        } else {
         //failtureCallback callback === 'function'
          failtureCallback && failtureCallback({ code: '-200', message: '请求失败' })
        }
      }, 2000)
    }

    foo('failture',
      res => console.log(res),
      err => console.log(err)
    )
  • 回调函数处理异步的弊端
    • 如果是我们自己封装的函数,那么我们在封装的时候必须要自己设计好callback名称, 并且使用好,否则容易造成不理解。
    • 如果我们使用的是别人封装的函数或者一些第三方库, 那么我们必须去看别人的源码或者文档, 才知道它这个函数需要怎么去获取到结果。
    • 异步嵌套太多,容易造成回调地狱,代码看起来很不容易理解,可维护可阅读性都不高,找bug要花很多时间。
 function foo(url, successCallback, failtureCallback) {
      // 假设这是网络请求,需要请求完毕才能返回给用户使用
      setTimeout(() => {
        if (url === 'success') {
          successCallback && successCallback({ code: '200', message: '请求成功' })
        } else {
          failtureCallback && failtureCallback({ code: '-200', message: '请求失败' })
        }
      }, 1000)
    }

    foo('success',
      // 成功回调
      (res) => {
        console.log(`第一次数据请求成功,接着请求第二次数据${res.code}`)
        foo('success', (res) => {
          console.log(`第二次数据请求成功,接着请求第三次数据${res.code}`)
          foo('success', (res) => {
            console.log(`第三次数据请求成功,接着请求第四次数据${res.code}`)
            foo('success', (res) => {
              console.log(`第四次数据请求成功,接着请求第五次数据${res.code}`)
              foo('success', (res) => {
                console.log(`第五次数据请求成功,接着请求第六次数据${res.code}`)
                foo('failture', (res) => {
                  console.log(`第六次数据请求成功,接着请求第七次数据${res.code}`)
                }, (err) => { console.log(`第六次数据请求失败,gg了${err.code}`) })
              }, (err) => { console.log(err) })
            }, (err) => { console.log(err) })
          }, (err) => { console.log(err) })
        }, (err) => { console.log(err) })
      },
      // 失败回调
      (err) => { console.log(err) })

上面代码就是典型的回调地狱,试问,如果你在真实项目开发中遇见这样的代码,你能够分析出问题的所在吗?所以这不是一种合理解决异步的方案。

Promise

Promise是一个类,可以翻译成承诺、许诺、期约。

    // resolve , reject 是两个回调函数
    // 当我们调用resolve的时候 , 会执行Promise对象的then方法传入的回调函数
    // 当我们调用reject的时候 , 会执行Promise对象的catch方法传入的回调函数
    const promise = new Promise((resolve, reject) => { })

Promise的状态一旦被确定下来,无法被更改,resolve、reject两个函数不会代码禁止向下执行,为了防止继续向下执行,要加上return。

Promise的三个状态

  • 待定( pending ) : 初始状态,既没有被兑现,也没有被拒
  • 已兑现( fulfilled ) : 意味着操作已经完成 resolve
  • 已拒绝 (rejected):意味着操作失败 reject

resolve不同值的区别

  • 如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数
  const promise = new Promise((resolve, reject) => { resolve("普通值") });

    promise.then(res => {
      console.log(res)
    }, err => { })
    
  • 如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态
     const promise = new Promise((resolve, reject) => {
      resolve(new Promise((resolve, reject) => {
        reject("我是新的promies, 我要改变之前promise的状态,这里会执行err")
      }))
    });

    promise.then(res => {
      console.log(res)
    }, err => {
      console.log(err)
    })
  • 如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据 then方法的结果来决定Promise的状态
    const promise = new Promise((resolve, reject) => {
      const obj = {
        name: 'obj',
        then(resolve, reject) {
          reject('传入一个对象,对象里面有then方法,会改变之前promise的状态')
        }
      }
      resolve(obj)
    });

    promise.then(res => {
      console.log(res)
    }, err => {
      console.log(err)
    })

then 方法的参数

    // then方法接受两个参数
    new Promise((resolve, reject) => {
      reject('嘿嘿')
    }).then(
      res => {
        console.log('resolve触发的回调函数');
      },
      err => {
        console.log('reject触发的回调函数');
      }
    )
    
    // 等价于
    new Promise((resolve, reject) => {
      reject('嘿嘿')
    })
      .then(res => { console.log('resolve触发的回调函数') })
      .catch(err => { console.log('reject触发的回调函数') })

then 方法多次调用

    const promise = new Promise((resolve, reject) => {
      reject('嘿嘿嘿')
    })

    promise.then(res => { console.log(res) }).catch(err => { console.log(err) })
    promise.then(res => { console.log(res) }).catch(err => { console.log(err) })

then/catch 方法返回值

then/catch方法本身返回的就是一个Promise,所以我们可以进行链式调用。

    // catch打印完后又返回了一个promise 所以后面的then又会打印
    new Promise((resolve, reject) => {
      reject('嘿嘿')
    })
      .then(res => { console.log('resolve触发的回调函数'); })
      .catch(err => { console.log('reject触发的回调函数'); return 123 })
      .then(res => { console.log(res) })

finally 方法

finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是reject状态,最终都会被执行的代码。

    new Promise((resolve, reject) => {
      reject('reject')
    })
      .then(res => { console.log(res) })
      .catch(err => { console.log(err) })
      .finally(() => { console.log('我是最后被执行的') })

all 方法

它的作用是将多个Promise包裹在一起形成一个新的Promise。新的Promise状态由包裹的所有Promise共同决定。

    // 当所有的Promise状态变成fulfilled状态时,新的Promise状态为fulfilled,并且会将所有Promise的返回值 组成一个数组; 
    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('1')
      }, 1000)
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('2')
      }, 2000)
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('3')
      }, 3000)
    })

    Promise.all([p3, p2, p1]).then(res => {
      console.log(res)
    }).catch(err => {
      console.log(err)
    })
    //当有一个Promise状态为reject时,新的Promise状态为reject,并且会将第一个reject的返回值作为参数;
    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('1')
      }, 1000)
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('2')
      }, 2000)
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('3')
      }, 3000)
    })

    Promise.all([p3, p2, p1]).then(res => {
      console.log(res)
    }).catch(err => {
      console.log(err)
    })

allSettled 方法

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

该方法会在所有的Promise都有结果(settled),无论是fulfilled,还是reject时,才会有最终的状态。

const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(11111)
      }, 1000);
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject(22222)
      }, 2000);
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(33333)
      }, 3000);
    })

    // allSettled
    Promise.allSettled([p1, p2, p3]).then(res => {
      console.log(res)
    }).catch(err => {
      console.log(err)

race 方法

如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法。

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('1')
      }, 1000)
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('2')
      }, 998)
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('3')
      }, 1002)
    })

    // 不管是resolve 还是reject  谁先有结果,那么就使用谁的结果
    Promise.race([p1, p2, p3])
      .then(res => { console.log(res) })
      .catch(err => { console.log(err) })

any 方法

和race方法是类似的,any方法会等到一个fulfilled状态,才会决定新Promise的状态,如果所有的Promise都是reject的,那么会报一个AggregateError的错误。

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('1')
      }, 1000)
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('2')
      }, 998)
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('3')
      }, 1002)
    })

    // 会打印p1 
    Promise.any([p1, p2, p3])
      .then(res => { console.log(res) })
      .catch(err => { console.log(err) })

如果全部reject

    const p1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('1')
      }, 1000)
    })

    const p2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('2')
      }, 998)
    })

    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('3')
      }, 1002)
    })

    // 那么会报一个AggregateError的错误。
    Promise.any([p1, p2, p3])
      .then(res => { console.log(res) })
      .catch(err => { console.log(err, err.errors) })
使用Promise重构回调函数(一)
     function foo(url) {
      return new Promise((resolve, reject) => {
        // 假设这是网络请求,需要请求完毕才能返回给用户使用
        setTimeout(() => {
          if (url === 'success') {
            resolve({ code: '200', message: '请求成功' })
          } else {
            reject({ code: '-200', message: '请求失败' })
          }
        }, 1000)
      })
    }

    foo('success').then(res => {
      console.log(`第一次请求成功,接着请求第二次${res.code}`)
      foo('success').then(res => {
        console.log(`第二次请求成功,接着请求第三次${res.code}`)
        foo('success').then(res => {
          console.log(`第三次请求成功,接着请求第四次${res.code}`)
          foo('success').then(res => {
            console.log(`第四次请求成功,接着请求第五次${res.code}`)
            foo('success').then(res => {
              console.log(`第五次请求成功,接着请求第六次${res.code}`)
              foo('failture').then(res => {
              }, err => {
                console.log(`第六次请求失败gggg${err.code}`)
              })
            }, err => { })
          }, err => { })
        }, err => { })
      }, err => { })
    }, err => { })

上面的代码属于把奔驰当做拖拉机在开,还是会有回调地狱的存在,接下来我们看看,Promise是如何解决回调地狱的。

使用Promise重构回调函数(二)
    function foo(url) {
      return new Promise((resolve, reject) => {
        // 假设这是网络请求,需要请求完毕才能返回给用户使用
        setTimeout(() => {
          if (url === 'success') {
            resolve({ code: '200', message: '请求成功' })
          } else {
            reject({ code: '-200', message: '请求失败' })
          }
        }, 1000)
      })
    }


    foo('success')
      .then(res => {
        console.log(`第一次请求成功,接着请求第二次${res.code}`)
        return foo('success')
      }, err => {
        console.log(`第一次请求失败,还是会执行下面的代码${err.code}`)
      })
      .then(res => {
        console.log(`第二次请求成功,接着请求第三次${res.code}`)
        return foo('success')
      }, err => {
        console.log(`第二次请求失败,还是会执行下面的代码${err.code}`)
      })
      .then(res => {
        console.log(`第三次请求成功,接着请求第四次${res.code}`)
        return foo('success')
      }, err => {
        console.log(`第三次请求失败,还是会执行下面的代码${err.code}`)
      })
      .then(res => {
        console.log(`第四次请求成功,接着请求第五次${res.code}`)
        // 这里让他失败!
        return foo('failture')
      }, err => {
        console.log(`第四次请求失败,还是会执行下面的代码${err.code}`)
      })
      .then(res => {
        console.log(`第五次请求成功,接着请求第六次${res.code}`)
        return foo('success')
      }, err => {
        console.log(`第五次请求失败,还是会执行下面的代码${err.code}`)
      })
      .then(res => {
        console.log(`第六次请求成功,接口请求完毕${res.code}`)
        return foo('success')
      }, err => {
        console.log(`第六次请求失败,还是会执行下面的代码${err.code}`)
      })

这里我们发现,我们解决了回调地狱的问题,但是出现了新的问题,代码量增加了很多。而且代码执行的中途遇见了错误,代码还是会继续执行。我们尝试使用catch(统一捕获错误)优化一下代码。

使用Promise重构回调函数(三)
  function foo(url) {
      return new Promise((resolve, reject) => {
        // 假设这是网络请求,需要请求完毕才能返回给用户使用
        setTimeout(() => {
          if (url === 'success') {
            resolve({ code: '200', message: '请求成功' })
          } else {
            reject({ code: '-200', message: '请求失败' })
          }
        }, 1000)
      })
    }


    foo('success')
      .then(res => {
        console.log(`第一次请求成功,接着请求第二次${res.code}`)
        return foo('success')
      })
      .then(res => {
        console.log(`第二次请求成功,接着请求第三次${res.code}`)
        return foo('success')
      })
      .then(res => {
        console.log(`第三次请求成功,接着请求第四次${res.code}`)
        return foo('success')
      })
      .then(res => {
        console.log(`第四次请求成功,接着请求第五次${res.code}`)
        return foo('success')
      })
      .then(res => {
        console.log(`第五次请求成功,接着请求第六次${res.code}`)
        // 故意让他失败
        return foo('failture')
      })
      .then(res => {
        console.log(`第六次请求成功,接着请求第七次${res.code}`)
        return foo('success')
      })
      .catch(err => { console.log(`反正就是报错了,由我统一来捕获错误,捕获到错误代码不会继续运行。`) })

使用catch统一捕获错误的优点就是代码量减少了,而且代码执行中途遇见错误,代码就不会继续往下面执行了,因为返回了一个reject被最后的catch给捕获了,但是这还不是最终的一个处理异步的方案。接下来我们来看看最终的一个解决方案。

使用Promise重构回调函数(四)
    function foo(url) {
      return new Promise((resolve, reject) => {
        // 假设这是网络请求,需要请求完毕才能返回给用户使用
        setTimeout(() => {
          if (url === 'success') {
            resolve({ code: '200', message: '请求成功' })
          } else {
            reject({ code: '-200', message: '请求失败' })
          }
        }, 1000)
      })
    }

    async function getData() {
      try {
        const res1 = await foo('success')
        console.log(`第一次请求成功,接着请求第二次${res1.code}`)
        const res2 = await foo('success')
        console.log(`第二次请求成功,接着请求第三次${res2.code}`)
        const res3 = await foo('success')
        console.log(`第三次请求成功,接着请求第四次${res3.code}`)
        const res4 = await foo('failture')
        console.log(`第四次请求成功,接着请求第五次${res4.code}`)
        const res5 = await foo('success')
        console.log(`第五次请求成功,接着请求第六次${res5.code}`)
        const res6 = await foo('success')
        console.log(`第六次请求成功,接着请求第七次${res6.code}`)

        console.log(`---------------------------end--------------------------------`)
      } catch (err) {
        console.log(`反正就是失败了,如果遇见失败,代码不会继续向下执行`)
      }
    }
    
    getData()

我们使用了async和await,来解决回调地狱、then调用代码冗余问题,这是一个最终的解决方案,在实际开发项目中,我们都是使用的这个方案。