第11章 异步编程
本章节主要介绍Promise的特性和使用,优雅地执行异步任务。
# 异步编程
同步任务
对应内存中顺序执行的处理器指令。上一条指令执行完成后,可以立即得到作用后的状态。比如,
let x = 1 // 初始化定义 x += 1 // 更改值 console.log(x) // 获取值 2
1
2
3同步意味着顺序,阻塞,也给出明确的结果。
异步任务
类似系统中断,即代码在当前进程外部执行。好处在于任务不会阻塞,可以并行执行,但同样不容易推断结果。
let x = 1 // 初始化定义 setTimeout(() => { x += 1 // 更改值 }, 0) console.log(x) // 获取值 1
1
2
3
4
5以往是通过回调函数的方式来获取异步任务的结果,不过这会引发著名的“回调地狱”。
function test(callback) { let x = 1 // 初始化定义 setTimeout(() => { x += 1 // 更改值 callback(x) }, 0) } test((res) => { console.log(res) // 获取值 2 })
1
2
3
4
5
6
7
8
9
10
11
# Promise
ES6新增的一个引用类型Promise,使用方式如下
let p = new Promise(()=>{}) // 传入执行器函数
# Promise状态机
- pending 待定状态
- resolved 解决状态,附带一个解决值。
- rejected 拒绝状态,附带一个拒绝理由。
注意这3种状态都有可能存在,并且不可修改和直接通过js检测。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ok')
}, 0)
})
console.log(p1) // Promise { <pending> }
setTimeout(() => {
console.log(p1) // Promise { 'ok' }
}, 100)
2
3
4
5
6
7
8
9
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('ok')
}, 0)
})
console.log(p1) // Promise { <pending> }
setTimeout(() => {
console.log(p1) // UnhandledPromiseRejectionWarning: ok
}, 100)
2
3
4
5
6
7
8
9
# Promise.resolve()
该静态方法可以直接返回一个resolved状态的Promise实例,如果有值,会包装后返回。下面两个实例是等价的。
let p1 = new Promise((resolve) => resolve())
let p2 = Promise.resolve()
2
此外该方法是一个幂等方法。
let p = Promise.resolve(3)
setTimeout(() => {
console.log(p === Promise.resolve(p)) // true
console.log(p === Promise.resolve(Promise.resolve(p))) // true
}, 0)
2
3
4
5
# Promise.reject()
与Promise.resolve()用法类似,区别是非幂等的,它接收的参数都会作为拒绝promise的理由。
let p1 = new Promise((resolve, reject) => reject())
let p2 = Promise.reject()
2
let p = Promise.reject(Promise.resolve(10))
console.log(p) // Promise { <rejected> Promise { 10 } }
// UnhandledPromiseRejectionWarning: #<Promise>
2
3
注意try/catch不能捕获异步模式的错误,只能捕获同步模式的错误。
promise的错误只能通过promise的方法或者async/await下的try/catch来捕获。
try {
throw {
a: '1',
}
} catch (error) {
console.log('error:', error) // error: { a: '1' }
}
async function test() {
try {
// await Promise.reject({ reason: 'promise is not return' })
Promise.reject({ reason: 'promise is not return' })
.then((res) => {
console.log('res', res)
})
.catch((e) => {
console.log('e', e) // e { reason: 'promise is not return' }
})
} catch (err) {
console.log('err:', err)
}
}
test()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Promise.prototype.then()
接收最多2个回调参数,第一个回调在进入resolved状态时执行,第二个回调在进入rejected状态时执行。返回值为新的promise实例。
let p = new Promise((resolve, reject) => resolve(3))
p.then(
(result) => {
console.log(result)
},
(error) => {
console.log(error)
}
)
2
3
4
5
6
7
8
9
# Promise.prototype.catch()
相当于Promise.prototype.then(null,onRejected)
的语法糖。
let p = Promise.reject()
p.catch(() => {
console.log('rejected')
})
2
3
4
# Promise.prototype.finally()
在promise切换为resolved或rejected时都会执行。一个完整的promise调用如下:
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
})
p.then((res) => {
console.log(res)
})
.catch((err) => {
console.log(err)
})
.finally(() => {
// 无法获取是否resolved或rejected
console.log('finally')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 执行顺序
let p = new Promise((resolve) => {
console.log(1)
resolve('promise')
console.log(2)
})
p.then(() => {
console.log(4)
})
console.log(3)
// 1 2 3 4
2
3
4
5
6
7
8
9
10
11
12
再看个复杂点的
setTimeout(() => {
console.log(5)
}, 0)
Promise.resolve().then(() => {
console.log(3)
})
let p = new Promise((resolve) => {
setTimeout(() => {
console.log(6)
resolve('promise')
console.log(7)
}, 0)
Promise.resolve().then(() => {
console.log(4)
})
console.log(1)
})
p.then(() => {
console.log(8)
})
console.log(2)
// 1 2 3 4 5 6 7 8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
5是异步宏任务,挂起
3是异步微任务,挂起
6,7是异步宏任务,挂起
4是异步微任务,挂起
1是同步任务,执行 输出1
8是异步微任务,此时还未resolved,不执行
2是同步任务,执行 输出2
执行优先级:微任务>宏任务,输出3
输出4 此时8也是微任务,但Promise还没有扭转状态
同优先级的异步任务,看谁先在任务队列等候。 输出5 输出6
扭转Promise状态后,7在任务队列等待执行,8会让它先出列执行。输出7 8
注意第11步,这个顺序有Js运行时保证,被称为“非重入”特性。
处理程序会等到运行的消息队列让它出列时才执行。
# 拒绝错误处理
promise可以以任何理由拒绝,包括undefined,但最好统一使用错误对象。因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,方便调试。
正常情况下,通过throw()关键字抛出错误时,JS运行时的错误处理机制会停止执行抛出错误之后的任何指令:
function outputError() {
throw Error('foo')
console.log('bar') // 不执行
}
outputError()
console.log(1) // 不执行
2
3
4
5
6
但是,在promise中,异步错误是从消息队列中抛出的,不会阻止运行时继续执行同步指令:
function rejectError() {
Promise.reject(Error('foo')) // UnhandledPromiseRejectionWarning: Error: foo
console.log('bar') // bar
}
rejectError()
console.log('next ') // next
2
3
4
5
6
7
8
# 异步串行
其实就是.then().then()
结构,举个例子
function delay(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time * 1000)
})
}
function run() {
delay(1)
.then(() => {
console.log(1)
return delay(1)
})
.then(() => {
console.log(2)
return delay(1)
})
.then(() => {
console.log(3)
})
}
run()
// 1 (1秒后)
// 2 (2秒后)
// 3 (3秒后)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
改写下,变得更简洁。
function delay(time, str) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(str)
resolve()
}, time * 1000)
})
}
function run() {
delay(1, 1)
.then(() => delay(1, 2))
.then(() => delay(1, 3))
}
run()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用async/await,更简洁。
async function run() {
await delay(1, 1)
await delay(1, 2)
await delay(1, 3)
}
2
3
4
5
# 异步并行
# Promise.all()
创建的Promise会在一组promise全部解决之后再解决。使用如下:
let p = Promise.all([
new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 1000)
}),
Promise.resolve(2),
])
p.then((res) => {
console.log(res) // [1,2] (1秒钟后)
})
2
3
4
5
6
7
8
9
10
11
let p = Promise.all([
new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 1000)
}),
Promise.resolve(2),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(3)
}, 1000)
}),
Promise.reject(4),
])
p.then((res) => {
console.log('result', res)
}).catch((err) => {
console.log('reject:', err) // 返回第一个reject
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
注意:
- 如果所有promise都成功解决,则Promise.all的解决值就是包含所有promise解决值的数组,顺序不变。
- 如果有promise拒绝,则第一个拒绝的promise会将自己的理由作为Promise.all的拒绝理由。
# Promise.race()
返回一个包装promise,是一组集合中最先解决或拒绝的promise镜像。
let p = Promise.race([
new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(2)
}, 1000)
}),
])
p.then((res) => {
console.log('result', res)
}).catch((err) => {
console.log('reject:', err) // reject: 2
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
即不管是resolved还是rejected,Promise.race只看谁第一个落定promise,就已谁作为值返回。
# Promise扩展
ES6还未涉及的2个特性:promise取消和进度追踪。本文就不细说了。
# 异步函数
即async/await。
# async
用于声明异步函数,大多情况下异步函数和同步函数区别不大,唯一区别在于return。
异步函数,如果return返回了值,那这个值会被Promise.resolve()包装成一个promise对象。
async function foo() {
console.log(3)
return 1
}
// 等价于
// function foo() {
// console.log(3)
// return Promise.resolve(1)
// }
foo().then((res) => {
console.log(res)
})
console.log(2)
// 3 2 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意,此时foo返回值是一个promise对象,而不是数值1。
async function foo() {
console.log(3)
return 1
}
const a = foo()
console.log(a)
console.log(2)
// 3
// Promise { 1 }
// 2
2
3
4
5
6
7
8
9
10
11
注意,在异步函数中抛出错误会返回拒绝的promise。
async function foo() {
console.log(1)
throw 3
}
foo().catch(console.log)
console.log(2)
// 1 2 3
2
3
4
5
6
7
8
但是,拒绝promise的错误不会被异步函数捕获。
async function foo() {
console.log(1)
Promise.reject(3)
}
foo().catch(console.log)
console.log(2)
// 1
// 2
// UnhandledPromiseRejectionWarning: 3
2
3
4
5
6
7
8
9
10
11
加一个return或者await 就可以了。
async function foo() {
console.log(1)
return Promise.reject(3)
// await Promise.reject(3)
}
foo().catch(console.log)
console.log(2)
// 1
// 2
// 3
2
3
4
5
6
7
8
9
10
11
12
不过最好不要混用,async/await捕获错误可以与try/catch配合使用。
async function foo() {
console.log(1)
try {
await Promise.reject(3)
console.log(4) // 这行代码不会执行
} catch (err) {
console.log('err', err)
}
}
foo()
console.log(2)
// 1
// 2
// err 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# await
async/await就是*/yield的语法糖,可以暂停和恢复代码执行。
await不能单独使用,必须有async一起。
看下几个例子来理解await是如何暂停和恢复代码执行的。
async function foo() {
const msg = await Promise.resolve('foo')
console.log(msg)
}
async function bar() {
console.log(await 'bar')
}
async function baz() {
console.log('baz')
}
foo()
bar()
baz()
// baz
// foo
// bar
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JS运行时在碰到await关键字时,会记录在哪里暂停执行。等await右边的值可用了,JS运行时会向消息队列中推送一个任务,这个任务在恢复异步函数的时候执行。注意await前后的区别。
async function foo() {
console.log(2)
await null
console.log(4)
}
console.log(1)
foo()
console.log(3)
// 1 2 3 4
2
3
4
5
6
7
8
9
解释如下:
- 打印1
- 调用异步函数foo()
- 打印2
- await关键字暂停执行,得到立即可用的值null,向消息队列中推送一个任务
- foo()退出
- 打印3
- 同步线程代码执行完毕
- JS运行时从消息队列中取出任务,恢复异步函数foo()执行
- await取得null值
- 打印4
- foo()返回
再看个更复杂的例子:
async function foo() {
console.log(2)
console.log(await Promise.resolve(8))
console.log(9)
}
async function bar() {
console.log(4)
console.log(await 6)
console.log(7)
}
console.log(1)
foo()
console.log(3)
bar()
console.log(5)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意,《高程》的输出和解释有误,答案应该是
1 2 3 4 5 8 9 6 7
亲测,nodejs和浏览器环境都是如此。再来一道头条的面试题。
async function async1() {
console.log('1')
await async2()
console.log('2')
}
async function async2() {
console.log('3')
}
console.log('4')
setTimeout(function () {
console.log('5')
}, 0)
async1()
new Promise(function (resolve) {
console.log('6')
resolve()
}).then(function () {
console.log('7')
})
console.log('8')
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
注意并不是说遇到await后,右侧代码就不执行了,await是在等待,它等的是右侧的表达式的值,如果这个求值过程中有同步代码,那就会立即执行。等拿到右侧的值,则会将其推到任务队列中,等待主线程代码执行完毕后再取出,恢复代码执行。
# 异步函数实践
# sleep
async function sleep(delay) {
return new Promise((resolve) => setTimeout(resolve, delay))
}
async function foo() {
const t0 = Date.now()
await sleep(1500) // 暂停1500ms
const t1 = Date.now()
console.log(t1 - t0)
}
foo()
2
3
4
5
6
7
8
9
10
11
12
# 平行加速
假设有5个请求,每个耗时在0-1000ms之间,连续请求总共耗时是多少。
方案一:
串行请求,每个请求完成之后请求下一个,保证了顺序,但总耗时太长。
async function randomDelay(id) {
const delay = Math.random() * 1000
return new Promise((resolve) => {
setTimeout(() => {
console.log(`${id} finished`)
resolve(id)
}, delay)
})
}
async function foo() {
const t0 = Date.now()
for (let i = 0; i < 5; i++) {
await randomDelay(i)
}
console.log(`${Date.now() - t0} elapsed`)
}
foo()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
方案二:
一次性初始化promise,分别收集结果。
async function foo() {
const t0 = Date.now()
// for (let i = 0; i < 5; i++) {
// await randomDelay(i)
// }
let promises = Array(5)
.fill(null)
.map((_, i) => randomDelay(i))
for (let i = 0; i < 5; i++) {
console.log(`await ${await promises[i]}`)
}
console.log(`${Date.now() - t0} elapsed`)
}
foo()
// 3 finished
// 1 finished
// 2 finished
// 0 finished
// await 0
// await 1
// await 2
// await 3
// 4 finished
// await 4
// 922 elapsed
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
方案三:
使用Promise.all()
async function foo() {
const t0 = Date.now()
// for (let i = 0; i < 5; i++) {
// await randomDelay(i)
// }
let promises = Array(5)
.fill(null)
.map((_, i) => randomDelay(i))
const results = await Promise.all(promises)
for (let i = 0; i < results.length; i++) {
console.log(`await ${results[i]}`)
}
console.log(`${Date.now() - t0} elapsed`)
}
foo()
// 4 finished
// 0 finished
// 1 finished
// 3 finished
// 2 finished
// await 0
// await 1
// await 2
// await 3
// await 4
// 704 elapsed
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