第十四章 Promise对象

Promise的含义

所谓Promise简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
Promise对象有以下两个特点:
1.对象的状态不受外界影响。Promise对象代表一个异步操作,有3种状态:Pending(进行中)、Fulfilled(Resolved已成功)和Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

异步操作正在进行就会触发Pending,异步操作返回结果成功调用Resolved,异步操作返回结果失败调用Rejected,Resolved和Rejected就是用来改变Promise状态的。

2.一旦状态改变就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次如果不设置回调函数,Promise内部抛出的错误不会反应到外部,最后当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

基本用法

ES6规定,Promise对象是一个构造函数,用来生成Promise实例。

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

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由javascript引擎提供,不用自己部署。

一般构造函数接收对象或者基本类型数据作为参数,但是Promise对象接受函数作为参数,这个函数也是回调函数。

resolve函数的作用是,将Promise对象的状态从"未完成"变成"成功"(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果作为参数传递给then函数第一个回调函数的参数;reject函数的作用是,将Promise对象的状态从"未完成"变成"失败"(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误作为参数传递给then函数第二个回调函数的参数。
Promise实例生成以后,可以用then方法分别指定Resolved状态和Rejected状态的回调函数。

promise.then(function (value) {
    // success
}, function (error) {
   // error
})

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Rejected时调用。其中第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

let promise = new Promise(function (resolve, reject) {
     console.log('Promise')
     resolve()
})
promise.then(function () {
     console.log('Resolve')
})
console.log('Hi')

// Promise
// Hi
// Resolve
上面代码中,Promise新建后会立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数将在当前脚本所有同步任务执行完成后才会执行,所以Resolve输出。
下面是一个用Promise对象实现的AJAX操作的例子。

let getJSON = function (url) {
    let promise = new Promise(function (resolve, reject) {
         let client = new XMLHttpRequest()
         client.open('GET', url)
         client.onreadystatechange = handler
         client.responseType = 'json'
         client.setRequestHander('Accept', 'application/json')
         client.send()

        function handler() {
            if (this.readyState !== 4) {
                return
            }
            if (this.readyState === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
    })
    return promise
}

getJSON('/posts.json').then(function (json) {
     console.log('Contents:' + json)
}, function (error) {
     console.log('出错了', error)
})

注意:调用resolve或reject并不会终结Promise的参数函数执行,后面的代码还是会继续执行,所以最好在它们前面加上return语句,这样就不会产生意外。

Promise.prototype.then()

Promise实例具有then方法,即then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。
then方法返回的是一个新的Promise实例(注意:不是原来的Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法,如果返回的不是Promise实例是不可以用链式调用的。

getJSON('/post/1.json').then(function (post) {
     return getJSON(post.commentURL)
}).then(function funcA(comments) {
     console.log('Resolved:', comments)
}, function funcB(error) {
     console.log('Rejected:', error)
})

上面方法中,第一个then方法指定的回调函数返回的是另一个Promise对象。这时第二个then方法指定的回调函数就会等待这个新的Promise对象状态发生变化。

Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, reject)的别名,用于指定发生错误时的回调函数,如果异步操作抛出错误,状态就会变为Rejected,然后调用catch方法指定的回调函数处理这个错误。
一般用catch方法,因为它可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此建议使用catch方法,而不使用then方法的第二个参数。

Rejected和catch区别:这两者可以互换,几乎等价,这两个同时存在的时候只会触发第一个出现的;如果有多个Promise前者只能捕获指定的上一个Promise的报错,但是catch可以捕获最初的报错。

注意:catch方法返回的是一个新的Promise实例,执行完之后这个新的Promise状态就会变成Resolved,所以catch方法后面还可以跟then()方法。

Promise.all()

Promise.all方法用于将多个Promise实例包装成一个新的Promise实例。

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

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例;如果不是,就会先调用下面讲到的Promise.resolve,将参数转为Promise实例,再进一步处理(Promise.all方法的参数不一定是数组,但是必须具有Iterator接口,且返回的每个成员都是Promise实例)
p的状态由p1、p2、p3决定,分成两种情况。
1.只有p1、p2、p3的状态都变成Resolved,p的状态才会变成Resolved,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
2.只要p1、p2、p3中一个被Rejected,p的状态就变成Rejected,此时第一个被Rejected的实例的返回值会传递给p的回调函数。

const dataBasePromise = connectDataBase()
const booksPromise = dataBasePromise.then(findAllBooks)
const userPromise = dataBasePromise.then(getCurrentUser)
Promise.all([
     booksPromise,
     userPromise
]).then([books, user] => {
    pickTopRecomment(books, user)
}) 

上面代码中,booksPromise和userPromise是两个异步操作,只有它们的结果都返回,才会触发pickTopRecomment回调函数。
注意:如果作为参数的Promise实例自身定义了catch方法,那么实例执行完catch方法后就会变成Resolved,导致Promise.all方法参数里面的两个实例都会Resolved,因此它被rejected时并不会触发Promise.all()的catch方法。

Promise.race()

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

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

上面代码中,只要p1、p2、p3中有一个实例率先改变状态,p的状态就会跟着改变。那个率先改变的Promise实例的返回值就传递给p的回调函数,不管结果本身是成功状态还是失败状态。

Promise.resolve()

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

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

Promise.resolve方法的参数分成以下4种情况:

参数是一个Promise实例

如果参数是一个Promise实例,那么Promise.resolve将不做任何修改,原封不动地返回这个实例。

参数是一个thenable对象

thenable对象指的是具有then方法的对象,比如下面这个对象。

let thenable = {
    then: function (resolve, reject) {
         resolve(42)
    }
}
let p = Promise.resolve(thenable)
p.then((value) => console.log(value))
// 42

Promise.resolve方法会将这个对象转为Promise对象,然后立即执行thenable对象的then方法,对象p的状态就变为Resolved,从而立即执行最后的then方法指定的回调函数,输出42。

参数不是具有then方法的对象或根本不是对象

如果参数是一个原始值,或者是一个不具有then方法的对象,那么Promise.resolve方法返回一个新的Promise对象,状态为Resolved。

let p = Promise.resolve('Hello')
p.then((value) => {console.log(value)})
// Hello
不带有任何参数

Promise.resolve方法允许在调用时不带有参数,而直接返回一个Resolved状态的Promise对象。所以如果希望得到一个Promise实例对象,比较方便的方法就是直接调用Promise.resolve方法。

Promise.reject()

Promise.reject方法也会返回一个新的Promise实例,状态为Rejected。

let p = Promise.reject('出错了')
// 等同于
let p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, (error) {console.log(error)})
// 出错了

上面的代码生成一个Promise对象的实例p,状态为Rejected,回调函数会立即执行。

两个有用的附加方法

ES6的PromiseAPI提供的方法不是很多,可以自己部署一些有用的方法。下面部署两个不在ES6中但很有用的方法。

done()

无论Promise对象的回调链以then方法还是catch方法结尾,只要最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。为此,我们可以提供一个done方法,它总是处于回调链的尾端,保证抛出任何可能出现的错误。下面是它的实现

Promise.prototype.done = function (resolve, reject) {
     this.then(resolve, reject).catch(function (reason) {
          // 抛出一个全局错误
          setTimeout(() => {throw reason}, 0)
     })
}

由上可见,done方法可以像then方法那样使用,提供Resolved和Rejected状态的回调函数,也可以不提供任何参数。

finally()

finally方法用于指定不管Promise对象最后状态如何都会执行的操作。它与done方法的最大区别在于,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
它的实现也很简单

Promise.prototype.finally = function (callback) {
    let p = this.constructor
    return this.then(
         value => p.resolve(callback()).then(() => value),
         reason => p.resolve(callback()).then(() => { throw reason })
    )
}

应用

加载图片

我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

const preloadImage = function (path) {
     return new Promise(function (resolve,reject) {
          let image = new Image()
          image.onload = resolve
          image.onerror = reject
          image.src = path
    })
}

Generator函数与Promise的结合

Promise.try()

实际开发中经常遇到一种情况:不知道或者不想区分函数f是同步函数还是异步操作,想用Promise来处理它,但f如果是同步函数,用Promise处理就会变成异步操作。那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且可以统一的处理。下面有两种方式可以实现,第一种写法是使用async函数

const f = () => console.log('now')
(async () => f())().then().catch()
console.log('next')
// now 
//next

第二种写法是使用new Promise()

const f = () => console.log('now')
(
   () => new Promise(
        resolve => resolve(f())
   )
)()
console.log('next')
// now 
// next

现在ES6提供了Promise.try方法替代上面的写法。

const f = () => console.log('now')
Promise.try(f).then().catch()
console.log('next')
// now
// next

你可能感兴趣的:(第十四章 Promise对象)