Promise

Promise 的特点

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变就不会再变,任何时候得到的都是这个结果。Promise 对象的状态改变只有两个可能:从 Pending 变为 Fulfilled,从 Pending 变为 Rejected。只要这两种情况发生,这时就成为 resolve。就算改变已经发生,再对 Promise 对象添加回调函数,也会立即得到这个结果。与 Event 完全不同,Event 一旦错过再监听是得不到结果的。

基本用法

1
2
3
4
5
6
7
8
9
var promise = new Promise(function (resolve, reject) {
//some code

if (/*异步操作成功*/) {
resolve(value);
} else {
reject(error);
}
})

resolve 函数的作用是,将 Promise 对象的状态从 Pending 变为 Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject 函数的作用是,将 Promise 对象的状态从 Pending 变为 Rejected,将报出的错误传递出去。
Promise 实例生成后,可以用 then 方法分别制定 Resolve 状态和 Rejected 状态的回调函数:

1
2
3
4
5
6
7
8
promise.then(
function (value) {
//success
},
function (error) {
//failure
},
)

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

1
2
3
4
5
6
7
8
9
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done')
})
}
timeout(100).then((value) => {
console.log(value)
})
//'done'

过了 ms 后,Promise 实例的状态变为 Resolved,触发 then 方法绑定的回调函数。
Promise 新建后就会立即执行:

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

//Promise
//hi
//Resolve

then 方法指定的回调函数将在当前脚本所有同步任务执行完成后才会执行,所以 Resolve 最后输出。
异步加载图片:

1
2
3
4
5
6
7
8
9
10
11
12
function loadImageAsync(url) {
return new Promise(function (resolve, reject) {
var image = new Image()
image.onload = function () {
resolve(image)
}
image.onerror = function () {
reject(new Error('Could not load image at ' + url))
}
image.src = url
})
}

使用 Promise 实现 AJAX:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var getJSON = function (url) {
var promise = new Promise(function (resolve, reject) {
var client = new XMLHttpRequest()
client.open('GET', url)
client.onreadystatechange = handler
client.responseType = 'json'
client.setRequestHeader('Accept', 'application/json')
client.send()

function handler() {
if (this.readyState !== 4) {
return
}
if (this.status === 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.error('出错了', error)
},
)

以 Promise 对象作为 resolve 的参数

1
2
3
4
5
6
7
var p1 = new Promise(function (resolve, reject) {
//...
})
var p2 = new Promise(function (resolve, reject) {
//...
resolve(p1)
})

此时 p1 的状态决定了 p2 的状态。如果 p1 的状态是 Pending,那么 p2 的回调就会等待 p1 的改变;如果 p1 的状态已经是 Resolved 或者 Rejected,那么 p2 的回调函数就会立即执行。

1
2
3
4
5
6
7
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2.then((result) => console.log(result)).catch((error) => console.log(error))

p1 三秒后变为 Rejected,p2 在一秒后变为 Resolved,由于 p2 返回的是另一个 Promise,所以 p2 的状态无效,由 p1 的状态决定 p2 的状态。后面的 then 语句都变成针对 p2 的,再过两秒,p1 变为 Rejected,触发 catch 指定的回调函数。
调用 resolve 或 reject 并不会结束 Promise 函数的执行
因为立即 resolve 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。所以最好在前面加上 return 语句。

Promise.prototype.catch()

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
getJSON('/posts.json')
.then(function (posts) {
//...
})
.catch(function (error) {
//处理getJSON和前一个回调函数运行时发生的错误
})

p.then((val) => console.log('fulfilled:', val)).catch((err) =>
console.log('rejected:', err),
)
//等同于
p.then((val) => console.log('fulfilled:', val)).then(null, (err) =>
console.log('rejected', err),
)

var promise = new Promise(function (resolve, reject) {
throw new Error('test')
})
promise.catch(function (error) {
console.log(error)
})

//写法一
var promise = new Promise(function (resolve, reject) {
try {
throw new Error('test')
} catch (e) {
reject(e)
}
})
promise.catch(function (error) {
console.log(error)
})

//写法二
var promise = new Promise(function (resolve, reject) {
reject(new Error('test'))
})
promise.catch(function (error) {
console.log(error)
})

比较可知 reject 方法的作用等同于抛出错误。
如果 Promise 状态已经变成 Resolved,再抛出错误是无效的

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

Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。一般来说,应用 Promise 的 catch 方法。
与传统的 try/catch 不同的是,如果没有使用 catch 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,不会有任何反应。需要注意的是,catch 返回的还是一个 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var someAsyncThing = function () {
return new Promise(function (resolve, reject) {
resolve(x + 2) //ReferenceError
})
}
someAsyncThing()
.catch(function (error) {
console.log('error:', error)
})
.then(function () {
console.log('carry on')
})
//Error: ReferenceErro x is not defined
//carry on

如果没有报错,会跳过 catch 方法。catch 方法中还能抛出错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var someAsyncThing = function () {
return new Promise(function (resolve, reject) {
resolve(x + 2) //ReferenceError
})
}
someAsyncThing()
.then(function () {
return someOtherAsyncThing()
})
.catch(function (error) {
console.log(error)
y + 2 //ReferenceError
})
.catch(function (error) {
console.log('carry on', error)
})
//x in not defined
//carry on y is not defined

Promise.all

将多个 Promise 对象包装成一个新的实例。

1
var 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 的回调函数。
1
2
3
4
5
6
const databasePromise = connectDatabase()
const booksPromise = databasePromise.then(findAllBooks)
const userPromise = databasePromise.then(getCurrentUser)
Promise.all([booksPromise, userPromise]).then(([books, user]) =>
pickTopRecommentations(books, user),
)

只有 booksPromise 和 userPromise 结果都返回,才会触发 pickTopRecommentations 回调函数。
如果作为参数的 Promise 实例自身定义了 catch 方法,那么它被 rejected 时并不会触发 Promise.all 的 catch 方法

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

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

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

Promise.race

将多个 Promise 实例包装成一个新的实例。

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

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

1
2
3
4
5
6
7
8
9
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((response) => console.log(response))
p.catch((error) => console.log(error))
//五秒内fetch无法返回变量,p的状态就变为Rejected,从而触发catch方法的回调函数

Promise.resolve

  • 参数是一个 Promise 实例,不做任何修改,返回这个实例
  • 参数是一个 thenable 对象,将这个对象转为 Promise 对象,然后立即执行 thenable 对象的 then 方法
1
2
3
4
5
6
7
8
9
10
let thenable = {
then: function (resolve, reject) {
resolve(42)
},
}
let p1 = Promise.resolve(thenable)
p1.then(function (value) {
console.log(value)
})
//42
  • 参数根本不是具有 then 方法的对象或者不是对象,返回一个新的 Promise 对象,状态为 Resolved
1
2
3
4
5
var p = Promise.resolve('hello')
p.then(function (s) {
console.log(s)
})
//'hello'
  • 不带有任何参数,直接返回一个 Resolved 状态的对象
  • Promise.reject 返回一个状态为 Rejected 的 Promise 对象

立即 resolve 的 Promise 对象是在本轮事件循环结束时,而不是在下次事件循环开始时

done

只要最后一个方法抛出错误,都有可能无法捕捉到,为此可以配置一个 done 方法。

1
asyncFunc().then(f1).catch(r1).then(f2).done()

实现的代码如下:

1
2
3
4
5
6
7
Promise.prototype.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected).catch(function (reason) {
setTimeout(() => {
throw reason
}, 0) //抛出一个全局错误
})
}

finally

与 done 最大的区别在于,接受一个普通的回调函数作为参数,该函数不管怎样都执行。

1
2
3
4
5
6
server
.listen(0)
.then(function () {
// run test
})
.finally(server.stop)

实现代码如下:

1
2
3
4
5
6
7
8
9
10
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
}),
)
}

eg: 加载图片

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

Generator 函数与 Promise 结合

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function getFoo() {
return new Promise(function (resolve, reject) {
resolve('foo')
})
}
var g = function* () {
try {
var foo = yield getFoo()
console.log(foo)
} catch (e) {
console.log(e)
}
}
function run(generator) {
var 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) // 用run处理Promise对象,并调用下一个next方法

Promise chaining

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function loadJSON(url) {
return fetch(url).then((response) => response.json())
}

function loadUser(name) {
return fetch(`https://api.github.com/users/${name}`).then((response) =>
response.json(),
)
}

function showAvatar(user) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.src = user.avatar_url
img.className = 'example-img'
document.body.append(img)

setTimeout(() => {
img.remove()
resolve(loadUser) // here can resolve another promise
}, 3000)
})
}

loadJSON('test.json')
.then((user) => loadUser(user.name))
.then(showAvatar)
.then((user) => console.log(`github user: ${user}`))
// ... and so on