概念
Promise 是异步编程中的一种解决方案,比传统方案回调函数和事件 更合理也更强大。
Promise是一个容器,里面保存着某个未来才会结束的事件的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。
三种状态:
pending (进行中)、fulfilled(已成功)、rejected(失败)。
好处:
使用Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
Promise 对象提供了统一的接口,使得控制异步操作容易。
缺点:
无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
当处于pending 状态时,无法得知目前进展到哪一个阶段。
基本用法
ES6规定,Promise对象是一个构建函数,用来生成Promise实例。
const promise = new Promise(function(resolve, reject) => {
if (/*异步操作成功*) {
resolve(value)
} else {
reject(error)
}
})
promise.then(function(value) {
}, function(error) {
})
then 方法可以接受两个回调函数作为参数。
第一个回调 fulfilled 时调用。
第二个回调 rejected 时调用,是可选函数。
let promise = new Promise((resolve, reject) => {
console.log('Promise')
resolve()
})
promise.then(function() {
console.log('resolved.')
})
console.log('Hi')
// Promise
// Hi
// resolved
异步加载图片
function asyncLoadImage(url) {
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = function () {
resolve(image)
}
image.onerror = function () {
reject(new Error("Could not load Image at" + url))
}
image.src = url
})
}
let imgUrl = "https://dss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=2534506313,1688529724&fm=26&gp=0.jpg"
let ssuccessImage = asyncLoadImage(imgUrl).then((r) => {
console.log(r)
let box = document.querySelector("#box")
box.append(r)
})
// 使用Promise 实现Ajax 操作的例子
const getJSON = function(url) {
const promise = new Promse((resolve, reject) => {
const handler = function() {
if (this.readyState !== 4) {
return
}
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
const client = new XMLHttpRequest()
client.open('GET', url)
client.onreadystatechange = handler;
client.reponseTpye = 'json'
client.setRequestHeader('Accept', 'application/json')
client.send()
})
return promise
}
getJSON('/posts.json').then(function(json) {
console.log('Contents: ' + json)
}, function(eror) {
console.log('出错了', error)
})
Promise.protoype.then()
Promise 实例具有then 方法,是为Promise 实例添加状态改变时的回调函数。
then方法第一个参数是 resolved(fulfilled) 状态的回调函数,第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise 实例,可采用链式写法。
getJSON('/posts.json').then(function(json) {
return json.post
}).then(function(post) {
...
})
第一个回调函数完成以后,会将其返回结果作为参数,传入第二个回调函数。
Promise.prototype.catch()
Promise.prototype.catch 是 .then(null, rejection) 或 .then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {
}).catch(function(error) {
console.log('发生错误!', error)
})
如果异步操作抛出错误,状态就会变为rejected, 就会调用catch方法指定的回调函数。另外,then 方法指定的回调函数,如果在运行中抛出错误, 也会被catch 方法捕获。
const promise = new Promise(function(resolve, reject) {
throw new Error('test')
})
promise.catch(function(error) {
console.log(error)
})
// Error: test
如果Promise状态已经变成 resolved(fulfilled),再抛出错误是无效的。
const promise = new Promsie(function(resolve, reject) {
resolve('ok')
throw new Error('test')
})
promise
.then(function(value) { console.log(value) })
.catch(function(value) { console.log(error) })
// ok
Promise 的状态一旦改变,就永久保持这个状态,不会再变了。
Promise 对象的错误具有”冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL)
}).then(function(comments) {
// ...
}).catch(function(error) {
// 处理前三个Promise 产生的错误
})
const promise = new Promise(function(resolve, reject) {
resolve('ok');
setTimeout(funtion() { throw new Error('test')}, 0)
})
promise.then(function(value) { console.log(value) })
// ok
// uncaught Erroe: test
上例的Promise指定在下一轮事件循环再抛出错误。到了那个时候,Promise的运行已经结束了,所以这个错误是在Promise函数体外抛出的,会冒泡到最外层,成了未捕获的错误。
一般总是建议,Promise对象后面要跟 catch 方法,这样可以处理Promise内部发生的错误。catch方法返回的还是一个Promise对象,因此后面还可以接着调用then 方法。
cosnt someAsyncThing = function() {
return new Promise(function(resolve, reject) {
resolve(x + 2)
})
}
someAsyncThing()
.catch((error) => { console.log('oh no', error) })
.then(() => { console.log('carray on') })
// oh no [ReferenceError: x is not defined]
// carray on
如果没有报错,则会跳过catch方法。
catch 方法中,还能再抛出错误
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
resolve(x + 2)
})
}
someAsyncThing().then(function() {
return someOtherAsyncThing()
}).catch(function(error) {
console.log('oh no', error)
y + 2
}).catch(fuction(error) {
console.log('carray on', error)
})
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
Promise.prototype.finally()
finally 方法用于指定(不管最后状态如何的)Promsie 对象都会执行的回调函数。
promise
.then(result => {...})
.catch(err => {...})
.finally(() => {...})
finally 方法的回调函数不接受任何参数,所以无法知道之前的Promise状态到底是fulfilled 还是 rejected。 这表明finally 方法里面的操作,应该是与状态无关的,不依赖于Promise 的执行结果。
finally 方法总是会返回原来的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
Promise.all()
Promise.all 方法用于将多个Promise 实例,包装成一个新的Promise 实例。
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 的回调函数。
const databasePromise = connectDatabase()
const booksPromise = databasePromise.then(findAllBooks)
const userPromise = databasePromise.then(getCurrentUser)
Promise.all([booksPromise, userPromise])
.then(([books, user]) => pickToRecommendations(books, user))
booksPromise 和 userPromise 是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommentdations 这个回调函数。
注意: 如果作为参数的Promise 实例,自己定义了catch 方法,那么它一旦被 rejected,并不会触发Promise.all() 的 catch 方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello')
}).then(result => result)
.catch(e => e)
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了')
}).then(result => result)
.catch(e => e)
Promise.all([p1, p2])
.ten(result => console.log(result))
.catch(err => console.log(e))
// ['hello', Error: 报错了]
上例: p1 会 resolved, p2 首先会rejected, 但p2 有自己的catch 方法,该方法返回的是一个新的Promise 实例, p2 指向的是这个实例。该实例执行完catch 方法后,也变成resolved, 导致 Promise.all 方法参数里面的两个实例都会resolved, 因此会调用then 方法指定的回调函数,而不会调用catch 方法指定的回调函数。
如果p2 没有自己的catch 方法, 就会调用 Promise.all() 的catch 方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello')
}).then(result => result)
.catch(e => e)
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了')
}).then(result => result)
Promise.all([p1, p2])
.ten(result => console.log(result))
.catch(err => console.log(e))
// Error: 报错了
Promise.race()
Promise.race 方法同样是将多个Promise 实例,包装为一个新的Promise 实例。
const p = Promise.race([p1, p2, p3])
只要p1, p2, p3之中有一个实例率先改变,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race 方法的参数,如果不是Promise 实例,就会先调Promise.resolve 方法,将参数转为Promise 实例,再进一步处理。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(console.log('fetch')).catch(console.error('error'))
上例: 如果5秒内fetch方法无法返回结果,变量p的状态就会变为 rejected,从而触发catch 方法指定的回调函数。
Promise.resolve()
有时需要将现有对象转为Promise 对象,Promise.resolve 方法就起到了这个作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'))
上例: 将JQuery 生成的deferred 对象,转为一个新的Promise 对象。
Promise.resove('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve 方法的参数的四种情况:
- 参数是一个Promise 实例
如果参数是Promise 实例,那么Promise.resolve 将不做任何修改,原封不动地返回这个实例。
- 参数是一个thenabel对象
thenable 对象指的是具有then 方法的对象。如:
Promise.resolve 方法会将 thenable 对象转为Promise对象,然后立即执行thenable对象的then 方法。
let thenable = {
then: function(resolve, reject) {
resolve(42)
}
}
let p1 = Promise.resolve(thenable)
p1.then(function(value) {
console.log(value) // 42
})
上例: thenable 对象的then方法执行后,对象p1 的状态就变成resolved,从而立即执行最后那个 then 方法指定的回调函数,输出42.
- 参数不是具有then方法的对象,或根本就不是对象
如果参数是一个原始值,或是一个不具有then 方法的对象,则Promise.resolve 方法返回一个新的Promise 对象,状态为resolved.
const p = Promise.resolve('Hello')
p.then(function(s) {
console.log(s) // Hello
})
不带任何参数
Promise.resolve 方法允许调用时不带参数,直接返回一个resolved 状态的Promise对象。
setTimeout(function() {
console.log('three')
}, 0)
Promise.resolve().then(function() {
console.log('two')
})
console.log('one')
// one
// two
// three
上例: setTimeout(fn, 0) 在下一轮 事件循环 开始时执行,Promise.resolve() 在本轮 事件循环结束时执行,console.log(‘one’)则是立即执行。
Promise.reject()
Promise.reject(reason) 方法会返回一个新的Promise 实例,该实例的状态为rejected.
const p = Promise.reject('出错了')
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(s) {
console.log(s)
})
// 出错了
上例生成一个Promise对象的实例p, 状态为 rejected, 回调函数会立即执行。
注意: Promise.reject() 方法的参数,会原封不动地作为reject 的理由,变成后续方法的参数。这一点与Promise.resolve 方法不一致。
const thenable = {
then(resolve, reject) {
reject('出错了')
}
}
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上例,Promise.reject 方法的参数是一个thenable 对象,执行之后,后面catch方法的参数不是reject抛出的’出错了‘这个字符串’,而是thenable 对象。
Promise.try()
使 同步函数同步执行,异步函数异步执行,并且让它们具有统一的API.
第一种: async函数
const f = () => console.log('now')
(async () => f())();
console.log('next')
// now
// next
第 2 行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f 是同步的,就会得到同步的结果;如果f 是异步的,就可以用 then 指定下一步。
(async () => f())().then(...)
注意: async () => f() 会吃掉f() 抛出的错误。所以,如果想捕获错误,要使用Promise.catch 方法。
(async () => f())()
.then(...)
.catch(...)
第二种写法使用 new Promise()
const f = () => console.log('now')
(
() => new Promise(resolve => resolve(f()))
)()
console.log('next')
// now
// next
上例是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。
鉴于这是一个很常见的需要,所以现有一个提案,提供 Promise.try 方法替代上面的写法。
const f = () => console.log('now')
Promise.try(f)
console.log('next')
// now
// next
由于Promise.try 为所有操作提供了统一的处理机制,所以如果想用then 方法管理流程,最好都用Promise.try 包装一下。(可以更好地管理异常)
function getUsername(userId) {
return database.users.get({id: userId})
.then(function(user) {
return user.name
})
}
对错误的处理:
database.users.get({id: userId})
.then(...)
.catch(...)
// database.users.get() 返回一个Promise对象,如果抛出异步错误,可用catch 捕获。
try {
database.user.get({id: userId})
.then(...)
.catch(...) // 捕获异步错误
} catch (e) {
}
// 用 try...catch 捕获 database.users.get() 抛出的同步错误。
上面的写法太笨拙,这是可以统一用 promise.catch() 捕获所有同步和异步的错误。
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
事实上, Promise.try 就是模拟 try 代码块, 就像Promise.catch 模拟catch 代码块。
应用
加载图片
将图片的加载写成一个Promise, 一旦加载完成, Promise 的状态就发生变化。
const preloadImage = function(path) {
return new Promise(function(resolve, reject) {
const image = new Image()
image.onload = resolve;
image.onerror = reject;
image.src = path;
})
}
Generator 函数与Promise 的结合
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。
function getFoo() {
return new Promise(function(resolve, reject) {
resolve('foo')
})
}
const g = function* () {
try {
const foo = yield getFoo()
console.log(foo)
} catch(e) {
console.log(e)
}
}
function run(generator) {
const it = generator()
function go(result) {
if (result.done) return result.value
return result.value.then(function(value) {
return go(it.next(value))
}, function(error) {
return go(it.throw(error))
})
}
go(it.next())
}
run(g)
函数 g 有一个异步操作getFoo, 它返回的就是一个 Promise 对象,函数run 用来处理这个Promise对象,并调用下一个next 方法。