Promise对象学习笔记

251 阅读9分钟

Promise对象简介

Promise是异步编程的一种解决方案,比传统的解决方案-回调函数和事件-更合理更强大。简单讲,Promise里储存着某个未来才会结束的事件的结果,从它可以获取异步操作的消息。 Promise对象有以下两个特点:

  • 对象的状态不受外界影响。Promise有三种状态:Pending(进行中),Fulfilled(已成功),Rejected(已失败)。只有异步操作的结果才能决定当前是哪种状态。
  • 一旦状态改变就不会再变化,并且Promise的变化只会有两种情况:Pending变成Fulfill或Pending变成Rejected。只要这两种情况发生,则一直保持这个结果,此时成为Resolved(已定型。下文中为保持统一,Resolved代指Fulfilled状态,不包含Rejected状态)。

Promise可以方便的将异步操作以同步操作的流程表达出来,避免层层嵌套的回调函数。

基本用法

先创建一个Promise实例:

const promise = new Promise((resolve, reject) {
    // ...some code
    if(/*异步操作完成*/) {
        resolve(value)
    else {
        reject(error)
    }
})

Promise构造函数接收一个函数作为参数,该函数的两个参数分别是resolve函数和reject函数。resolve函数的作用是将Promise的状态从pending变成resolved,在异步操作成功时调用,并将结果作为参数传递出去;reject函数的作用时将Promsie的状态从pending变成rejected,在异步操作失败时调用,并将异步操作的错误作为参数传递出去。

Promise的then方法可以分别指定Resolved状态和rejected状态的回调函数。其中第一个回调作为Resolved状态的回调,第二个回调作为rejected状态的回调,而第二个回调是可选的。

执行顺序

下面我们来猜一猜下面的代码的打印顺序是什么样的:

let promise = new Promise((resolve, reject)=>{
    console.log('Promise');
    resolve()
})
promise.then(() => {
    console.log('Resolved');
})
console.log('Hi')

让我们先来推理一下:首先,Promise构造函数中的代码会在新建的时候立即执行,所以,第一个打印的应该是“Promise”;其次then方法指定的回调函数会在当前脚本所有同步任务执行完之后才执行,所以“Resolved”应该是最后打印的。总上所述。打印顺序应该是

// Promise
// Hi
// Resolved

还有一点需要注意:resolve和reject并不会阻止后面代码的执行,并且后面的代码还会先执行

new Promise((resolve, reject) => {
    resolve(1);
    console.log(2);
}).then((res) => {
    console.log(res)
})
// 2
// 1

这是因为执行resolved是在本次事件循环的末尾执行,总是晚于本次循环的同步任务。 一般来说,不应该在resolve或reject之后写代码,应该将其放在then方法里面。为了预防万一,我们可以在resolve和reject前加上return语句。

给resolve传递Promise实例作为参数的情况

请看下面的例子,来猜一猜最后会进入then还是catch呢:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(()=> resolve(p1), 1000)
})
p2.then((res)=> {console.log(res)}).catch((err) => {console.log(err)});

// Error:fail

答案是会进入catch。这是因为p1的状态会传递给p2,也就是说p1的状态决定了p2的状态,如果p1是pending那么p2的回调会等待p1的状态改变;如果p1的状态是resolved或rejected,那么p2的回调会立即执行。所以上面的代码会在3秒后触发catch指定的回调。

Promise.prototype.then()

then方法的作用是为Promise实例添加状态改变时的回调函数。它的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
注意:then方法返回的是一个新的Promise实例(注意不是原来的Promise实例)。因此可以采用链式写法,即.then方法后面再调另一个.then。并且上一个then方法返回的结果会作为回调方法的参数。

采用链式的then方法可以依次指定一组按照顺序调用的回调函数,并且前一个回调函数可能返回的还是一个Promise实例,而后面的回调函数则会等待该Promise对象状态发生改变时再被调用。看下面代码你就懂了:

getJSON('/posts.json')
.then(post => getJSON(post.commentURL))
.then(
comments => {console.log('resolved', comments)},
err => {console.log('rejected', err)}
)

上面的代码getJSON返回的时一个Promise对象。所以第一个then返回的就是一个Promise,此时第二个then方法指定的回调函数就会等待这个新的Promise对象状态发生改变时,resolved就调用第一个回调,rejected就会调用第二个回调。

Promise.prototype.catch()

catch方法是用于指定发生错误时的回调函数。注意它不止可以捕获Promise抛出的错误还能捕获前一个回调函数运行时发生的错误。

getJSON('/posts.json').then(res => {
    throw new Error('error')
}).catch(err => {
    console.log(err)
})
// Error error

上面的代码中,getJSON这个Promise对象的状态若为resolved,会进入then方法,若为rejected则会进入catch方法。另外,如果then方法指定的回调在运行中抛出错误也会被catch捕捉。

Promise对象的错误具有'冒泡'的性质,会一直向后传递,直到被捕获,也就是说错误总是会被下一个catch语句捕获。

getJSON('/posts.json').then(post => {
    return getJSON(post.commentURL)
}) .then(comments => {
    // somecode
}).catch(err => {
    // 处理前面三个Promise产生的错误
})

我们一般使用catch捕获错误,而不使用then方法的第二个参数。

需要注意的是,catch方法返回的还是一个Promise对象,因此后面还能接着调用then方法。

若没有指定catch方法处理错误,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。

Promise.all()

Promise.all方法用于将多个Promise实例包装成一个新的Promise实例。Promise.all方法接收的参数必须具有Iterator接口且返回的每个成员都必须是Promise实例。若不是Promise实例,Promise.all方法内部就会先执行Promise.resolve方法将参数转为Promise实例。

let p = Promise.all([p1,p2,p3])

上面代码中,p的状态由p1,p2,p3决定,分为两种情况:
1、只有p1、p2、p3的状态都是Fulfilled,p的状态才会是Fulfilled,此时,p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
2、只要p1、p2、p3任一个状态为rejected,则p的状态为rejected,返回第一个被rejected的实例的返回值给p的回调函数。

看下面的例子:

const promises = [1,2,3].map(id => {
    return getJSON('/post' + id + '.json')
});
Promise.all(promises).then((res) => {
    // ...
}).catch(err => {
    // ...
})

如果作为参数的Promise实例自身定义了catch方法,那么它被rejected时并不会触发Promise.all的catch方法。 看下面例子:

const p1 = new Promise((resolve,reject) => {
    resolve('hello')
}).then((res) => res)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
    throw new Error('error!')
}).then(res => res)
.catch(err => err);

Promise.all([p1,p2])
.then(res => console.log(res))
.catch(err => {console.log(err)})
// ["hello", Error: error!]

上面的代码中p2会rejected,但是Promise.all()会进入then的回调,这是因为p2有自己的catch方法,而该方法返回的是一个新的Promise实例,p2实际上指向的是这个实例,而这个实例执行完之后也会变成resolved,所以all方法中的两个实例都是resolved,因此不会调用catch方法。

Promise.race()

race方法同样是将多个Promise实例包装成一个新的实例。

let p = Promise.race([p1,p2,p3])

不同之处是race方法的状态在第一个参数实例率先改变状态后,p的状态就会跟着改变,并将那个率先改变的实例的返回值传递给p的回调。同all方法一样,若参数中有不是Promise实例的,会先调用resolve方法,将参数转换成Promise实例再进一步处理。

用下面的例子介绍下race的使用场景:

const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise((resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(res => console.log(res));
p.catch(err => console.log(err));

上面的代码中,如果5秒内fetch方法无返回结果,则p的catch回调就会被触发。

Promise.resolve()

有时候需要将现有的对象转换成Promise.resolve对象,这时就要用到Promise.resolve方法。

Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'))

Promise.resolve方法的参数分成以下四种情况: 1、Promise实例
如果参数时Promise实例,那么Promise.resolve将不作任何修改,直接返回这个实例。

2、thanable对象
thanable对象是指具有then方法的对象,比如下面这个:

let thenable = {
    then: (resolve, reject) => {
        resolve(42)
    }
}

Promise.resolve方法会将这个对象转换成Promise对象,并立即执行thenable对象的then方法。

3、不具有then方法的对象或者根本不是对象
如果参数是一个原始值或者不具有then方法的对象,那么Promise.resolve返回的是一个新的Promise对象,状态为rejected,Promise.resolve方法的参数会同时传给回调函数,并且回调会立即执行。

4、不带任何参数 不带任何参数的话,resolve会直接返回一个带有resolved状态的Promise对象,需要注意的是,立即resolved的Promise对象是在本轮事件循环结束时执行,而不是在下一轮事件循环开始时执行。

Promise.reject()

Promise.reject方法也会返回一个新的Promise实例,状态为Rejected。与Promise.resolve方法不同的一点是,reject方法的参数会原封不动的作为reject的理由变成后续方法的参数。如下:

const thenable = {
    then: (resolve, reject) => {
        reject('error')
    }
}
Promise.reject(thenable).catch(e => {
    console.log(e === thenable)
})
// true

上面代码中,Promise.reject方法的参数是一个thenable对象,而后面catch方法的参数不像resolve方法一样,抛出的‘error’字符串,而是这个thenable对象本身。

总结

这篇文章是对阮一峰老师的《ES6标准入门》的一个学习笔记吧,内容大多是来源于书中,在此整理一下方便后续复习巩固。对Promise有其他想法的同学欢迎评论指教哦。

参考

阮一峰 《ES6标准入门》