史上最详解Promise

850 阅读10分钟

案例初识Promise

  • Promise 是为了解决异步回调问题,避免回调地狱的异步解决方案;promise是在ES6中新增加的JavaScript标准的内置对象,它用来处理 异步操作 的;在ES6以前是通过使用 回调函数来处理异步操作后的结果,在处理过程中如果有 多个有顺序 的异步操作,会造成回调的嵌套,引发 回调地狱Promise是用来解决JS中进行异步编程的新的解决方案(旧的方案是单纯的使用回调函数,会造成回调地狱))。

举个例子:如果有两个异步操作(执行需要时间),分别是 砍柴 和 烧水,我们不妨将它们定义为下面的两个函数:

// 砍柴需要时间2s, 是个异步操作
function kanChai(fn) {
    setTimeout(() => {
        fn();   
    }, 2000)
}
// 烧水需要时间1s, 是个异步操作
function shaoShui(fn) {
    setTimeout(() => {
        fn();
    }, 1000)
}

其中fn是回调函数,定义了砍柴和烧水这两个异步操作 执行完成后 要做的事情。如果现在要求这两个异步操作必须按顺序执行先砍柴再烧水,可不可以这么写呢?

(function func() {
	// 先砍柴
	kanChai(() => {
	     console.log("砍柴完成")
	 });
	// 后烧水
	shaoShui(() => {
	    console.log("烧水完成");
	})
})();
  • 上面这样写的话我猜肯定是出于人的日常思维先调用砍柴函数再调用烧水函数,但这两个函数却是两个异步函数,会被异步执行,定时任务结束后里面的函数体会被添加入事件队列中,然后js线程才会去事件队列中依次取出执行。

也就是说砍柴和烧水两个函数里面的定时器函数在定时器线程里面是同步执行的,但是由于烧水函数里面的定时器用时短,会先被移入事件队列中会,以至于js线程会先执行烧水的完成的代码。所以会先输出"烧水完成"。显然并不是我们要的结果。我们想要的是先砍柴完成-烧水完成。

会有上面的思维就会有相应的解决方法;正确的写法应该把烧水嵌套在砍柴内:

(function func() {
    kanChai(() => {
        console.log("砍柴完成");
        shaoShui(() => {
            console.log("烧水完成");
        })
    })
})();

但是如果这是一个流程很多的事情这样一直嵌套就会出现下面这样的情况: image.png 相信大家很容易看出这种方式的弊端:如果有 多个有顺序的 异步操作,则会不停的嵌套,造成 回调地狱:即 砍柴完成后要做的事情(砍柴的回调) 和 烧水完成后要做的事情(烧水的回调) 被堆到了一块,造成代码可读性和可维护性极差。这就很容易造成回调地狱

而promise的出现,将 砍柴完成后要做的事情 和 烧水完成后要做的事情 从代码层面上分离开来了。

下面就用Promise重构一下上面的代码:

function kanChai() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("砍柴完成");
        }, 2000)
    })
}

function shaoShui() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve("烧水完成");
        }, 1000)
    })
}

kanChai()
.then((data) => {
    console.log(data);
    return shaoShui();
})
.then((data) => {
    console.log(data);
})

我们通过先直接调用砍柴函数kanChai,这个函数return了一个Promise,当定时2秒后会执行resolve("砍柴完成"),这时会自动调用.then方法,随即打印砍柴完成后又会 return一个shaoShuiPromise最后打印烧水完成

早期异步任务的处理

通过一个发送网络请求的实际例子作为切入点(网络请求就是一个异步任务)

  • 我们调用一个函数,这个函数中发送网络请求(此处用定时器来模拟);
  • 如果发送网络请求成功了,那么告知调用者发送成功,并且将相关数据返回过去;
  • 如果发送网络请求失败了,那么告知调用者发送失败,并且告知错误信息;
/**
 * 这种回调的方式有很多的弊端:
 *  1> 如果是我们自己封装的requestData,那么我们在封装的时候必须要自己设计好callback名称, 并且使用好
 *  2> 如果我们使用的是别人封装的requestData或者一些第三方库, 那么我们必须去看别人的源码或者文档, 
 *     才知道它这个函数需要怎么去获取到结果
 */

// request.js
function requestData(url, successCallback, failtureCallback) {
  // 模拟网络请求
  setTimeout(() => {
    // 拿到请求的结果
    // url传入的是coderwhy, 请求成功
    if (url === "coderwhy") {
      // 成功
      let names = ["abc", "cba", "nba"]
      successCallback(names)
    } else { // 否则请求失败
      // 失败
      let errMessage = "请求失败, url错误"
      failtureCallback(errMessage)
    }
  }, 3000);
}

// main.js
requestData("kobe", (res) => {
  console.log(res)
}, (err) => {
  console.log(err)
})

什么是Promise呢?

早期异步网络请求处理方案的缺陷

在上面异步任务处理的解决方案中,我们确确实实可以解决函数得到结果之后,获取到对应的回调,但是它存在两个主要的问题:

  • 第一,我们需要自己来设计回调函数、回调函数的名称、回调函数的使用等;
  • 第二,对于不同的人、不同的框架设计出来的方案是不同的,那么我们必须耐心去看别人的源码或者文档,以 便可以理解它这个函数到底怎么用;

对Promise的理解

Promise是一个类,可以翻译成 承诺、许诺 、期约;它的出现就是为了解决异步回调问题,避免回调地狱。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

Promise的结构

在通过new创建Promise对象时,我们需要传入一个回调函数,我们称之为executor

  • 这个回调函数会被立即执行,并且被传入另外两个回调函数resolve、reject;
  • 当我们调用resolve回调函数时,会执行Promise对象的then方法传入的回调函数;
  • 当我们调用reject回调函数时,会执行Promise对象的catch方法传入的回调函数,也可能是then方法里面第二个回调函数;

Promise的三种状态:

  • pending:初始状态,待定;
  • fulfilled:已敲定/兑现 ;
  • rejected:已拒绝;

状态一旦确定将不再改变,只有两种变换方式:

  • pending -> fulfilled 代表成功
  • pending -> rejected 代表失败

image.png

executor是在创建new Promise时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数resolve, reject

resolve的参数**

  • 情况一:如果resolve传入一个普通的值或者对象,那么这个值会作为then回调的参数;如果函数没有return会自动return一个undefined
  • 情况二:如果resolve中传入的是另外一个Promise,那么这个新Promise会决定原Promise的状态(我的理解是并不是调用了resolve就确定了状态而是还要看是否有新的promise或者实现了then方法的对象):
  • 情况三:如果resolve中传入的是一个对象,并且这个对象有实现then方法,那么会执行该then方法,并且根据 then方法的结果来决定Promise的状态:

image.png

Promise的方法

.then()方法

MDN

  • 该方法是Promise对象上的方法:它是放在Promise的原型上的 Promise.prototype.then

then()方法返回一个Promise。它最多需要有两个参数:第一个参数为Promise成功的回调函数由,resolve()调用;第二个参数为失败的回调函数,由reject()调用。

  • 一个Promise的then方法是可以被多次调用的:每次调用都可以传入对应的fulfilled回调;当Promise的状态变成fulfilled的时候,这些回调函数都会被执行;
  • 该方法实现了链式调用:then 方法返回一个 Promise 对象,其允许方法链。

.catch()方法

MDN

  • 该方法也是Promise对象上的方法:也是放在Promise的原型上的Promise.prototype.catch catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。
  • 事实上catch方法也是会返回一个Promise对象的,所以catch方法后面我们可以继续调用then方法或者catch方法:

.finally()方法

MDN

  • finally()  方法返回一个 Promise。在 promise 结束时,无论结果是 fulfilled 或者是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在 then() 和 catch() 中各写一次的情况。

then、catch、finally方法都属于Promise的实例方法,都存放在Promise的prototype上。

下面几种方法为Promise的类方法,又叫做静态方法,只能由Promise类访问,static

resolve()方法

  • 有时候我们已经有一个现成的内容了,希望将其转成Promise来使用,这个时候我们可以使用 Promise.resolve 方法来完成。Promise.resolve的用法相当于new Promise,并且执行resolve操作:

image.png Promise.resolve()方法的参数和上面提到的参数实质上是一样的

reject()方法

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

all()方法

MDN

// 创建多个Promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(11111)
  }, 1000);
})

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

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(33333)
  }, 3000);
})
.
// 需求: 所有的Promise都变成fulfilled时, 再拿到结果
// 意外: 在拿到所有结果之前, 有一个promise变成了rejected, 那么整个promise是rejected,结果为第一个reject的参数
Promise.all([p2, p1, p3, "aaaa"]).then(res => {
  console.log(res)  //只有当所有都为fulfilled时才会执行.then [ 22222, 11111, 33333, 'aaaa' ]
}).catch(err => {
  console.log("err:", err)    // 当有一个rejected时就会执行.catch并且打印第一个rejected结果   5555
})

all方法有一个缺陷:当有其中一个Promise变成reject状态时,新Promise就会立即变成对应的reject状态。那么对于resolved的,以及依然处于pending状态的Promise,我们是获取不到对应的结果的;

allSettled()方法

MDN

  • 该 Promise.allSettled()  方法返回一个在所有给定的 promise 都已经fulfilledrejected后的 promise,并带有一个对象数组,每个对象表示对应的 promise 结果。
// 创建多个Promise
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,不管什么情况都不会执行.catch,会返回数组里面包括状态。
// [
//   { status: 'fulfilled', value: 11111 },
//   { status: 'rejected', reason: 22222 },
//   { status: 'fulfilled', value: 33333 }
// ]
Promise.allSettled([p1, p2, p3]).then(res => {
  console.log(res)
}).catch(err => {    // 不会执行
  console.log(err,555) 
})

race()方法

MDN

  • 如果有一个Promise有了结果,我们就希望决定最终新Promise的状态,那么可以使用race方法:race是竞技、竞赛的意思,表示多个Promise相互竞争,谁先有结果,那么就使用谁的结果;

image.png

any()方法

MND

// 创建多个Promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve(11111)
    reject(1111)
  }, 1000);
})

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

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

// any方法  至少等到一个.then,如果都执行完了没有fifuled,到最后会执行.catch
Promise.any([p1, p2, p3]).then(res => {
  console.log("res:", res)    // 当有一个fifuled就直接执行   5555
}).catch(err => {
  console.log("err:", err.errors)    
  // 内部自己new了一个Promise,可以通过err.errors拿到具体的值  [ 1111, 22222, 3333 ]
})

可借鉴文章