从需求到原理,你的最后一篇Promise笔记

54 阅读11分钟

回调函数

在学习Promise之前,我们先学习一下前置知识,异步和同步,回调函数

同步和异步

举个生活中的例子了解下同步和异步:

早上起来,不论先刷牙还是先洗脸,都要等一个事情完毕后才能进行下一项,这就是同步

把水烧上,不用等水烧开,就去刷牙,烧水不会阻塞刷牙的执行,这就是异步

JavaScript是单线程语言,这意味着它一次只能执行一个任务。但是,它支持同步和异步编程。同步编程意味着每行代码都按顺序一个接一个地执行。异步编程允许程序在等待特定任务完成时执行其他任务。

看一段简单的代码,有一个求和函数和打印函数,求和函数内部有一个定时器,我们来看看输出的结果:

function sum(a, b) {
  console.log("执行sum函数")
  setTimeout(() => {
    console.log("sum函数内的异步任务")
		return a + b
  }, 1000)
}

function log() {
  console.log("执行log函数")
}

let result = sum(1, 2)
log()
console.log(result)

Untitled.png

分析上面代码的执行步骤:

  1. 首先,当代码被执行时,会创建一个调用栈,称为主调用栈。主调用栈中的第一个函数是全局执行上下文。

  2. sum 函数进入调用栈,开始执行。

    在函数 sum 内部, console.log 是同步任务,立即执行输出 "执行sum函数"

    然后,setTimeout 函数被调用,并且一个箭头函数作为参数被传入。而 setTimeout 是异步任务,其内部的回调函数会被放到任务队列,等待主调用栈中的任务执行完毕后再进栈执行。

  3. 函数 log 进栈被调用,并输出 "执行log函数"

  4. console.log(result) 进栈执行,但result接收的是setTimeout中回调函数的返回值,而回调函数还没有执行,因此输出"undefined" 。这时,主调用栈中的任务已经全部执行完毕。

  5. 还记得在任务队列中等待执行的 setTimeout 的回调函数吗,事件循环会将队列中的任务移动到主调用栈中执行。回调函数开始执行,输出 "sum函数内的异步任务" 并返回 a+b

现在定时器是不会阻塞后面代码的执行了,但是又有一个问题,上面代码中用result接收定时器返回的结果,但是输出result时,定时器还在任务队列里排队,没有执行,因此为result为undefined。

我们不知道异步函数结果何时返回,也就无法使用返回值,函数就失去了意义。那么我们如何获取到异步函数返回的结果呢?这就需要回调函数出场了

回调函数

回调函数首先是一个函数,但它会作为参数被传到另一个函数(父函数)中,当父函数执行完毕后,回调函数才会执行

这就解决了我们不知道异步代码何时执行完毕的问题,因为回调函数在父函数执行完毕后才会执行,可以在回调函数中将执行结果赋值给一个变量,并将该变量作为参数传递给父函数。

用回调函数改造之前的代码:


    function sum(a, b, cb) {
      setTimeout(() => {
        const result = a + b
        cb(result) // 将结果作为回调函数的参数返回
      }, 0)
    }

    // 通过回调函数的参数获取异步函数的结果
    sum(1, 2, (result) => {
      console.log(result)
    })

在前端开发中,回调函数被广泛应用于处理异步操作。以下是常见的使用场景:

  • 定时器:setTimeoutsetInterval 函数都接受一个回调函数作为参数。
  • 事件监听器:事件监听器常常使用回调函数来响应事件。
  • AJAX 请求:在 AJAX 请求中,回调函数被用于处理异步请求的响应。
  • Promise 和 async/await:Promise 和 async/await 都是为了处理异步操作而出现的,它们的底层实现也依赖于回调函数。

现在我们可以通过回调函数获取异步函数的结果了,但又有一个问题,假设需要连续调用四个异步函数,且后一个要依赖于前一个的执行结果:

    sum(1, 2, result => {
        sum(result, 3, result => {
            sum(result, 4, result => {
                sum(result, 5, result => {
                    console.log(result)
                })
            })
        })
    })

当我们有n个不同的复杂异步函数,又层层嵌套,我们就会陷入”回调地狱“之中,增加了代码的复杂程度,可调式性差。于是,ES6 提出了 Promise,让我们可以更加简洁得处理异步代码。

Promise

我们先给出Promise的定义,它是异步编程的一种解决方案:

顾名思义,Promise是承诺,承诺它过一段时间会给你一个结果。从语法上讲,Promise是一个用来存储存储异步代码执行结果的对象

创建promise

Promise是构造函数,要想使用promise对象,要先new一个实例

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

构造函数接收一个回调函数作为参数,它会在创建Promise时被调用,调用时会传入两个参数,它们都是Promise内部定义好的回调函数:

  • resolve:异步操作执行成功后的回调函数,用来传递执行成功后的数据
  • reject:异步操作执行失败后的回调函数,用来传递执行出错时的信息
    // 在回调中直接调用异步代码,通过resolve或reject函数将异步代码执行结果存储到Promise中
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
    		resolve("success")
        // reject(new Error("Error message"))
      }, 1000)
    })

打印看看promise实例,我们可以发现PromiseState都变成了fulfilled,而成功或失败的结果都存储到了PromiseResult中

succ.png

fail.png

既然结果都存到PromiseResult中了,那我们可以直接通过promise.PromiseResult获取数据吗?输出的结果还是undefined,因为我们不知道异步代码返回结果的时机,而输出promise.PromiseResult是同步执行的

获取Promise中的数据

then

then是Promise实例上的方法,通过 promise.then 获取到存储在Promsie中的数据

还记得我们是通过回调函数 resolve 和 reject 存储的数据吗?相应的,要获取其中的数据,then的参数也要是两个回调函数来指定resolve和reject,数据会作为回调函数的参数传递:

  • resolve传递的数据(data),在第一个回调函数(cb1)的参数中接收,通常我们会在cb1中编写处理数据的代码
  • reject传递的出现异常时的错误信息(error),在第二个回调函数(cb2)的参数中接收,通常我们会在cb2中编写处理异常的代码
    promise.then(
      (data) => {
        console.log(data) // resolve传递的数据
      },
      (error) => {  
        throw error // reject传递的数据
      }
    )

我们给了then两个回调函数,then怎么知道要执行哪一个呢?

还记得我们打印过的promise对象吗, 其中有两个的属性:PromiseResultPromiseState ,关键在于 PromiseState ,它代表着promise对象的状态,共有三种状态:

  • pending(等待状态):当Promise创建时的初始值
  • fulfilled(已完成):通过resolve存储数据时的状态,表示异步操作已成功完成
  • rejected(已拒绝):通过reject报错时的状态,表示异步操作失败了

通过then读取数据时,实质上是为promise设置了两个回调函数,根据PromiseState状态的变化决定调用哪个函数:

  • PromiseState === pending,什么都不做,等待状态改变,即等待数据存储到promise中,
  • PromiseState === fulfilled,调用第一个回调函数
  • PromiseState === rejected,调用第二个回调函数

链式调用

现在我们不再需要回调函数来返回异步函数的执行结果了,取而代之的是:

返回一个promise对象,将异步代码写在promise中,执行的结果通过resolve存储到promise中,再then方法获取promise中的数据

    *function* sum(a, b){
        return new Promise((resolve, reject) => {
            setTimeout(()=>{
                resolve(a + b)
            }, 1000)
        })
    }

    sum(1, 2).then(result => {
        console.log(result) // 3
    })

但是怎么连续多次调用呢,刚逃出回调地狱,又要陷入promise地狱了吗?

    // promise地狱
    sum(1, 2).then(result => {
        sum(result, 3).then(result =>{
            sum(result, 4).then(result => {
                console.log(result)
            })
        })
    }) 

实际上想要实现promise的连续多次调用,只需要进行链式调用,即根据需要一直 .then 下去

    sum(1, 2)
      .then(result => sum(result, 3)) // return new Promise(sum(result, 3))
      .then(result => sum(result, 4))
      .then(result => console.log(result))

调用then或catch方法时,在我们看不到的地方,会返回一个新的promise对象,相当于有一行隐藏代码:return new Promise()

then或catch(后面讲)中回调函数的返回值会被存储到这个promise中,如果没有返回值则promise中也没有任何值

每一次.then,读取的都是上一次.then返回的新promise对象中的结果

catch

为了使代码逻辑更加清晰,一般会用promise对象的catch方法代替then中的第二个回调函数

和第二个回调函数的作用一样,catch也是用来指定reject的回调,当PromiseState === rejected,即异步操作失败时被调用

    const promise = new Promise((resolve, reject) => {
        reject("出错了")
    }).then(data => {
        console.log('resolved', data)
    }).catch(err => {
        console.log('rejected', err) // rejected 出错了
    })

那在链式调用中怎么处理错误呢?我们先来看一下通过reject存储的错误信息的处理流程:

还记得promise的三种状态吗?在对promise进行链式调用时,then或catch会根据状态决定是否执行。当异步代码执行出错时,状态为rejected,遇到.then,如果then中没有第二个回调函数,则then不会执行,而是将异常信息封装到新的Promise中进行传递,直到异常被then的第二参数或是catch处理,如果一直没有处理,则异常会向外抛出,程序报错

同样的,如果异步代码执行成功,状态为fulfilled,中途遇到catch,也不会执行其中的代码,而是去执行后续.then的第一个回调

简而言之,对Promise进行链式调用时,如果上一步传来的promise的状态不是当前方法(then或catch)想要的状态:

resolve() → PromiseState: fulfilled → then(data ⇒ {})

reject()/ throw new Error → PromiseState: rejected → catch

则跳过当前的方法,这就是Promise的值穿透现象

这种设计方式使得我们可以在任意的位置对Promise的异常进行处理,例如如下代码:

    function sum(a, b) {
      return new Promise((resolve, reject) => {
        if (Math.random() > 0.7) {
    	throw new Error // 直接抛错和reject()的结果是一样的,状态也会变为rejected
          // reject("出错了")
        }
        resolve(a + b)
      })
    }

    sum(1, 2)
      .then((result) => sum(result, 3))
      .then((result) => sum(result, 4))
      .then((result) => console.log(result))
      .catch((err) => console.log(err))

上例代码中,当随机数>0.7时会出现异常,我们无法确定出现异常的时机,但出现异常后所有的then在异常处理前都不会执行,错误信息会存入Promise中向下传递,所以我们可以将catch写在调用链的最后,这样无论哪一步出现异常,我们都可以在最后统一处理

Finally

finally也是Promise的实例方法之一,和then、catch不同,无论异步代码是否执行成功,finally中的回调函数总会执行,通常我们在finally中定义一些无论Promise正确执行与否都需要处理的工作

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("success")
        // reject(new Error("Error message"))
      }, 1000)
    })

    promise
      .then((data) => {
        console.log(data)
      })
      .catch((error) => {
        console.log(error)
      })
      .finally(() => {
        console.log("Promise complete")
      })

静态方法

Promise类提供了一些静态方法,让我们可以更简洁得操作promise

Promise.resolve

用来创建一个新的Promise实例,且直接通过resolve存入一个数据

    Promise.resolve(10)

    //等价于
    new Promise((resolve, reject) => {
        resolve(10)
    })

    //获取数据
    Promise.resolve(10).then(r => console.log(r))

Promise.reject

用来创建一个新的Promise实例,且直接通过reject存入一个数据。

    Promise.reject("错误")
    //等价于
    new Promise((resolve, reject) => {
        reject("错误")
    })
    //获取数据,要通过catch获取
    Promise.resolve(10).catch(r => console.log(r))

Promise.all

当我们要同时执行多个Promise,等他们都执行完毕后,再将其结果进行统一处理时,使用Promise.all

Promise.all 需要一个数组作为参数,数组中可以存放多个promise对象。调用后,all方法会返回一个新的promise,这个promise会等待数组中所有的promise都执行完后,将所有promise的结果封装到数组中返回

注意,数组中的prmise对象中只要有一个报错,Promise.all执行时就会报错

    *function* sum(a, b) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(a + b)
            }, 1000);
        })
    }

    Promise.all([sum(1, 1), sum(2, 2), sum(3, 3)])
        .then((result) => {
            console.log(result)
        })
    // [2, 4, 6]

Promise.allSettled

Promise.allSettled也接收一个promise对象组成的数组,并返回一个Promise对象,该对象会在数组里的所有promise都完成后,不论成功或失败,将各自promise的结果封装到对象中,组成数组返回

    const promises = [
      Promise.resolve(1),
      Promise.reject('error'),
      Promise.resolve(3),
    ];

    Promise.allSettled(promises)
      .then(results => {
        console.log(results);
      })

    /*
    [ { status: 'fulfilled', value: 1 }, 
    { status: 'rejected', reason: 'error' }, 
    { status: 'fulfilled', value: 3 } ]
    */

Promise.race

顾名思义,所有promise比赛执行速度,会返回首先执行完的Promise的执行结果,而忽略其他未执行完的Promise

    Promise.race([
        Promise.reject("出错了"),
        sum(1, 2),
        sum(3, 4)
    ]).then(res => {
        console.log(r)
    }).catch(err => {
        console.log(err)
    })

    // 出错了

Promise.any

返回第一个成功的Promise的执行结果,如果所有的Promise都失败才会返回一个错误信息。

    Promise.any([
        Promise.reject("出错了"),
        sum(1, 2),
        sum(3, 4)
    ]).then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err)
    })

    //3