概念

Promise 是异步编程中的一种解决方案,比传统方案回调函数和事件 更合理也更强大。

Promise是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。

三种状态:
pending (进行中)、fulfilled(已成功)、rejected(失败)。

好处:
使用Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
Promise 对象提供了统一的接口,使得控制异步操作容易。

缺点:
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
当处于pending 状态时,无法得知目前进展到哪一个阶段。

基本用法

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

  1. const promise = new Promise(function(resolve, reject) => {
  2. if (/*异步操作成功*) {
  3. resolve(value)
  4. } else {
  5. reject(error)
  6. }
  7. })
  8. promise.then(function(value) {
  9. }, function(error) {
  10. })

then 方法可以接受两个回调函数作为参数。
第一个回调 fulfilled 时调用。
第二个回调 rejected 时调用,是可选函数。

  1. let promise = new Promise((resolve, reject) => {
  2. console.log('Promise')
  3. resolve()
  4. })
  5. promise.then(function() {
  6. console.log('resolved.')
  7. })
  8. console.log('Hi')
  9. // Promise
  10. // Hi
  11. // resolved

异步加载图片

  1. function asyncLoadImage(url) {
  2. return new Promise((resolve, reject) => {
  3. const image = new Image()
  4. image.onload = function () {
  5. resolve(image)
  6. }
  7. image.onerror = function () {
  8. reject(new Error("Could not load Image at" + url))
  9. }
  10. image.src = url
  11. })
  12. }
  13. let imgUrl = "https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg"
  14. let ssuccessImage = asyncLoadImage(imgUrl).then((r) => {
  15. console.log(r)
  16. let box = document.querySelector("#box")
  17. box.append(r)
  18. })

// 使用Promise 实现Ajax 操作的例子

  1. const getJSON = function(url) {
  2. const promise = new Promse((resolve, reject) => {
  3. const handler = function() {
  4. if (this.readyState !== 4) {
  5. return
  6. }
  7. if (this.status === 200) {
  8. resolve(this.response)
  9. } else {
  10. reject(new Error(this.statusText))
  11. }
  12. }
  13. const client = new XMLHttpRequest()
  14. client.open('GET', url)
  15. client.onreadystatechange = handler;
  16. client.reponseTpye = 'json'
  17. client.setRequestHeader('Accept', 'application/json')
  18. client.send()
  19. })
  20. return promise
  21. }
  22. getJSON('/posts.json').then(function(json) {
  23. console.log('Contents: ' + json)
  24. }, function(eror) {
  25. console.log('出错了', error)
  26. })

Promise.protoype.then()

Promise 实例具有then 方法,是为Promise 实例添加状态改变时的回调函数。
then方法第一个参数是 resolved(fulfilled) 状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

then方法返回的是一个新的Promise 实例,可采用链式写法。

  1. getJSON('/posts.json').then(function(json) {
  2. return json.post
  3. }).then(function(post) {
  4. ...
  5. })

第一个回调函数完成以后,会将其返回结果作为参数,传入第二个回调函数。

Promise.prototype.catch()

Promise.prototype.catch 是 .then(null, rejection) 或 .then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

  1. getJSON('/posts.json').then(function(posts) {
  2. }).catch(function(error) {
  3. console.log('发生错误!', error)
  4. })

如果异步操作抛出错误,状态就会变为rejected, 就会调用catch方法指定的回调函数。另外,then 方法指定的回调函数,如果在运行中抛出错误, 也会被catch 方法捕获。

  1. const promise = new Promise(function(resolve, reject) {
  2. throw new Error('test')
  3. })
  4. promise.catch(function(error) {
  5. console.log(error)
  6. })
  7. // Error: test

如果Promise状态已经变成 resolved(fulfilled),再抛出错误是无效的。

  1. const promise = new Promsie(function(resolve, reject) {
  2. resolve('ok')
  3. throw new Error('test')
  4. })
  5. promise
  6. .then(function(value) { console.log(value) })
  7. .catch(function(value) { console.log(error) })
  8. // ok

Promise 的状态一旦改变,就永久保持这个状态,不会再变了。

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

  1. getJSON('/post/1.json').then(function(post) {
  2. return getJSON(post.commentURL)
  3. }).then(function(comments) {
  4. // ...
  5. }).catch(function(error) {
  6. // 处理前三个Promise 产生的错误
  7. })
  1. const promise = new Promise(function(resolve, reject) {
  2. resolve('ok');
  3. setTimeout(funtion() { throw new Error('test')}, 0)
  4. })
  5. promise.then(function(value) { console.log(value) })
  6. // ok
  7. // uncaught Erroe: test

上例的Promise指定在下一轮事件循环再抛出错误。到了那个时候,Promise的运行已经结束了,所以这个错误是在Promise函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

一般总是建议,Promise对象后面要跟 catch 方法,这样可以处理Promise内部发生的错误。catch方法返回的还是一个Promise对象,因此后面还可以接着调用then 方法。

  1. cosnt someAsyncThing = function() {
  2. return new Promise(function(resolve, reject) {
  3. resolve(x + 2)
  4. })
  5. }
  6. someAsyncThing()
  7. .catch((error) => { console.log('oh no', error) })
  8. .then(() => { console.log('carray on') })
  9. // oh no [ReferenceError: x is not defined]
  10. // carray on

如果没有报错,则会跳过catch方法。

catch 方法中,还能再抛出错误

  1. const someAsyncThing = function() {
  2. return new Promise(function(resolve, reject) {
  3. resolve(x + 2)
  4. })
  5. }
  6. someAsyncThing().then(function() {
  7. return someOtherAsyncThing()
  8. }).catch(function(error) {
  9. console.log('oh no', error)
  10. y + 2
  11. }).catch(fuction(error) {
  12. console.log('carray on', error)
  13. })
  14. // oh no [ReferenceError: x is not defined]
  15. // carry on [ReferenceError: y is not defined]

Promise.prototype.finally()

finally 方法用于指定(不管最后状态如何的)Promsie 对象都会执行的回调函数。

  1. promise
  2. .then(result => {...})
  3. .catch(err => {...})
  4. .finally(() => {...})

finally 方法的回调函数不接受任何参数,所以无法知道之前的Promise状态到底是fulfilled 还是 rejected。 这表明finally 方法里面的操作,应该是与状态无关的,不依赖于Promise 的执行结果。

finally 方法总是会返回原来的值

  1. // resolve 的值是 undefined
  2. Promise.resolve(2).then(() => {}, () => {})
  3. // resolve 的值是 2
  4. Promise.resolve(2).finally(() => {})
  5. // reject 的值是 undefined
  6. Promise.reject(3).then(() => {}, () => {})
  7. // reject 的值是 3
  8. Promise.reject(3).finally(() => {})

Promise.all()

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

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

p1, p2, p3 都是Promise 实例,如果不是,就会先调用 Promise.resolve 方法, 将参数转为Promise 实例,再进一步处理。(Promise.all 方法的参数可以不是数组, 但必须具有Iterator 接口,且返回的每个成员都是Promise实例)
<1> 只有 p1, p2, p3 的状态都变成 fulfilled , p 的状态才会变成fulfilled, 此时p1, p2, p3 的返回值组成一个数组,传递给p 的回调函数。
<2> 只要 p1, p2, p3 之中有一个被rejected, p 的状态就变成rejected, 此时第一个被reject 的实例的返回值, 会传递给 p 的回调函数。

  1. const databasePromise = connectDatabase()
  2. const booksPromise = databasePromise.then(findAllBooks)
  3. const userPromise = databasePromise.then(getCurrentUser)
  4. Promise.all([booksPromise, userPromise])
  5. .then(([books, user]) => pickToRecommendations(books, user))

booksPromise 和 userPromise 是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommentdations 这个回调函数。

注意: 如果作为参数的Promise 实例,自己定义了catch 方法,那么它一旦被 rejected,并不会触发Promise.all() 的 catch 方法。

  1. const p1 = new Promise((resolve, reject) => {
  2. resolve('hello')
  3. }).then(result => result)
  4. .catch(e => e)
  5. const p2 = new Promise((resolve, reject) => {
  6. throw new Error('报错了')
  7. }).then(result => result)
  8. .catch(e => e)
  9. Promise.all([p1, p2])
  10. .ten(result => console.log(result))
  11. .catch(err => console.log(e))
  12. // ['hello', Error: 报错了]

上例: p1 会 resolved, p2 首先会rejected, 但p2 有自己的catch 方法,该方法返回的是一个新的Promise 实例, p2 指向的是这个实例。该实例执行完catch 方法后,也变成resolved, 导致 Promise.all 方法参数里面的两个实例都会resolved, 因此会调用then 方法指定的回调函数,而不会调用catch 方法指定的回调函数。

如果p2 没有自己的catch 方法, 就会调用 Promise.all() 的catch 方法。

  1. const p1 = new Promise((resolve, reject) => {
  2. resolve('hello')
  3. }).then(result => result)
  4. .catch(e => e)
  5. const p2 = new Promise((resolve, reject) => {
  6. throw new Error('报错了')
  7. }).then(result => result)
  8. Promise.all([p1, p2])
  9. .ten(result => console.log(result))
  10. .catch(err => console.log(e))
  11. // Error: 报错了

Promise.race()

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

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

只要p1, p2, p3之中有一个实例率先改变,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race 方法的参数,如果不是Promise 实例,就会先调Promise.resolve 方法,将参数转为Promise 实例,再进一步处理。

  1. const p = Promise.race([
  2. fetch('/resource-that-may-take-a-while'),
  3. new Promise(function(resolve, reject) {
  4. setTimeout(() => reject(new Error('request timeout')), 5000)
  5. })
  6. ])
  7. p.then(console.log('fetch')).catch(console.error('error'))

上例: 如果5秒内fetch方法无法返回结果,变量p的状态就会变为 rejected,从而触发catch 方法指定的回调函数。

Promise.resolve()

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

  1. const jsPromise = Promise.resolve($.ajax('/whatever.json'))

上例: 将JQuery 生成的deferred 对象,转为一个新的Promise 对象。

  1. Promise.resove('foo')
  2. // 等价于
  3. new Promise(resolve => resolve('foo'))

Promise.resolve 方法的参数的四种情况:

  1. 参数是一个Promise 实例

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

  1. 参数是一个thenabel对象

thenable 对象指的是具有then 方法的对象。如:

Promise.resolve 方法会将 thenable 对象转为Promise对象,然后立即执行thenable对象的then 方法。

  1. let thenable = {
  2. then: function(resolve, reject) {
  3. resolve(42)
  4. }
  5. }
  6. let p1 = Promise.resolve(thenable)
  7. p1.then(function(value) {
  8. console.log(value) // 42
  9. })

上例: thenable 对象的then方法执行后,对象p1 的状态就变成resolved,从而立即执行最后那个 then 方法指定的回调函数,输出42.

  1. 参数不是具有then方法的对象,或根本就不是对象

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

  1. const p = Promise.resolve('Hello')
  2. p.then(function(s) {
  3. console.log(s) // Hello
  4. })
  1. 不带任何参数

    Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的Promise对象。

  1. setTimeout(function() {
  2. console.log('three')
  3. }, 0)
  4. Promise.resolve().then(function() {
  5. console.log('two')
  6. })
  7. console.log('one')
  8. // one
  9. // two
  10. // three

上例: setTimeout(fn, 0) 在下一轮 事件循环 开始时执行,Promise.resolve() 在本轮 事件循环结束时执行,console.log(‘one’)则是立即执行。

Promise.reject()

Promise.reject(reason) 方法会返回一个新的Promise 实例,该实例的状态为rejected.

  1. const p = Promise.reject('出错了')
  2. // 等同于
  3. const p = new Promise((resolve, reject) => reject('出错了'))
  4. p.then(null, function(s) {
  5. console.log(s)
  6. })
  7. // 出错了

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

注意: Promise.reject() 方法的参数,会原封不动地作为reject 的理由,变成后续方法的参数。这一点与Promise.resolve 方法不一致。

  1. const thenable = {
  2. then(resolve, reject) {
  3. reject('出错了')
  4. }
  5. }
  6. Promise.reject(thenable)
  7. .catch(e => {
  8. console.log(e === thenable)
  9. })
  10. // true

上例,Promise.reject 方法的参数是一个thenable 对象,执行之后,后面catch方法的参数不是reject抛出的’出错了‘这个字符串’,而是thenable 对象。

Promise.try()

使 同步函数同步执行,异步函数异步执行,并且让它们具有统一的API.
第一种: async函数

  1. const f = () => console.log('now')
  2. (async () => f())();
  3. console.log('next')
  4. // now
  5. // next

第 2 行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f 是同步的,就会得到同步的结果;如果f 是异步的,就可以用 then 指定下一步。

  1. (async () => f())().then(...)

注意: async () => f() 会吃掉f() 抛出的错误。所以,如果想捕获错误,要使用Promise.catch 方法。

  1. (async () => f())()
  2. .then(...)
  3. .catch(...)

第二种写法使用 new Promise()

  1. const f = () => console.log('now')
  2. (
  3. () => new Promise(resolve => resolve(f()))
  4. )()
  5. console.log('next')
  6. // now
  7. // next

上例是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。

鉴于这是一个很常见的需要,所以现有一个提案,提供 Promise.try 方法替代上面的写法。

  1. const f = () => console.log('now')
  2. Promise.try(f)
  3. console.log('next')
  4. // now
  5. // next

由于Promise.try 为所有操作提供了统一的处理机制,所以如果想用then 方法管理流程,最好都用Promise.try 包装一下。(可以更好地管理异常)

  1. function getUsername(userId) {
  2. return database.users.get({id: userId})
  3. .then(function(user) {
  4. return user.name
  5. })
  6. }

对错误的处理:

  1. database.users.get({id: userId})
  2. .then(...)
  3. .catch(...)
  4. // database.users.get() 返回一个Promise对象,如果抛出异步错误,可用catch 捕获。
  5. try {
  6. database.user.get({id: userId})
  7. .then(...)
  8. .catch(...) // 捕获异步错误
  9. } catch (e) {
  10. }
  11. // 用 try...catch 捕获 database.users.get() 抛出的同步错误。

上面的写法太笨拙,这是可以统一用 promise.catch() 捕获所有同步和异步的错误。

  1. Promise.try(() => database.users.get({id: userId}))
  2. .then(...)
  3. .catch(...)

事实上, Promise.try 就是模拟 try 代码块, 就像Promise.catch 模拟catch 代码块。

应用

加载图片

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

  1. const preloadImage = function(path) {
  2. return new Promise(function(resolve, reject) {
  3. const image = new Image()
  4. image.onload = resolve;
  5. image.onerror = reject;
  6. image.src = path;
  7. })
  8. }

Generator 函数与Promise 的结合

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

  1. function getFoo() {
  2. return new Promise(function(resolve, reject) {
  3. resolve('foo')
  4. })
  5. }
  6. const g = function* () {
  7. try {
  8. const foo = yield getFoo()
  9. console.log(foo)
  10. } catch(e) {
  11. console.log(e)
  12. }
  13. }
  14. function run(generator) {
  15. const it = generator()
  16. function go(result) {
  17. if (result.done) return result.value
  18. return result.value.then(function(value) {
  19. return go(it.next(value))
  20. }, function(error) {
  21. return go(it.throw(error))
  22. })
  23. }
  24. go(it.next())
  25. }
  26. run(g)

函数 g 有一个异步操作getFoo, 它返回的就是一个 Promise 对象,函数run 用来处理这个Promise对象,并调用下一个next 方法。