ES2017 标准(ES8)引入了 async 函数,使得异步操作变得更加方便。
Promise 的编程模型依然充斥着大量的 then 方法,虽然解决了回调地狱的问题,但是在语义方面依然存在缺陷,代码中充斥着大量的 then 函数,async/await 是为优化then 链而开发出来的。
含义
async 函数是Generator 函数的语法糖:将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
。
const gen = function* () {
const f1 = yield readFile('/etc/fstab')
const f2 = yield readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}
// 改成async 函数
const asyncReadFile = async function() {
const f1 = await readFile('/etc/fstab')
const f2 = await readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}
对 Generator 函数的改进:
-
内置执行器
Generator 函数的执行必须靠执行器,所以才有了co 模块,而async 函数自带执行器。
-
更好的语义
async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
-
更广的适用性
co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
-
返回值是 Promise
async 函数的返回值是 Promise 对象,可以用then 方法指定下一步的操作。
基本用法
async 函数返回的是一个状态为fulfilled
的Promise 对象,有无值看有无return 值,如果async 函数没有返回值,它会返回Promise.resovle(undefined)。可以使用then
方法添加回调函数。
当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name)
const stockPrice = await getStockPrice(symbol)
return stockPrice
}
getStockPriceByName('goog').then(function(result) {
console.log(result)
})
语法
Promise 对象状态变化
async 函数返回的 Promise 对象,必须等到内部所有await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return 语句或者抛出错误。只有async 函数内部的异步操作执行完,才会执行then 方法指定的回调函数。
await 命令
await 命令是then 命令的语法糖。
语法:await expression
expression => 要等待的 Promise
实例,Thenable 对象,或任意类型的值。
返回值 => 返回从 Promise
实例或 thenable 对象取得的处理结果。如果等待的值不符合 thenable,则返回表达式本身的值。
异常 => 拒绝(reject)的原因会被作为异常抛出。
async funciton f() {
return await 123 // 等同于return 123
}
f().then(v => console.log(v))
// await 命令后面是一个Sleep 对象的实例
// 这个实例不是 Promise 对象,但是因为定义了then 方法,await 会将其视为Promise 处理
class Sleep {
constructor(timeout) {
this.timeout = timeout
}
then(resolve, reject) {
const startTime = Date.now()
setTimeout(
() => resolve(Date.now() - startTime)
this.timeout
)
}
}
(async () => {
const sleepTime = await new Sleep(1000)
console.log(sleepTime) // 1000
})
async function f() {
await Promise.reject('出错了')
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// await 语句前面没有return,但是reject 方法的参数依然传入了catch 方法的回调函数。
// 这里如果在await 前面加上return,效果是一样的。
async function f() {
await Promise.reject('出错了')
await Promise.resolve('hello world') // 不会执行
}
// 前一个异步操作失败,也不中断后面的异步操作:
// 法一:可以将第一个await 放在try...catch 结构里面,不管这个异步操作是否成功,第二个await 都会执行
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {}
return await Promise.resolve('hello world')
}
f()
.then(v => console.log(v))
// hello world
// 法二:await 后面的Promise 对象再跟一个catch 方法,处理前面可能出现的错误
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e))
return await Promise.resolve('hello world')
}
f()
.then(v => console.log(v))
// 出错了
// hello world
当函数执行到 await 时,被等待的表达式会立即执行,所有依赖该表达式的值的代码会被暂停,并推送进微任务队列(microtask queue)。然后主线程被释放出来,用于事件循环中的下一个任务。
async function foo(name) {
console.log(name, "start")
await console.log(name, "middle")
console.log(name, "end")
}
foo("First")
foo("Second")
// 执行到 await 时,后面的代码就会整体被安排进一个新的微任务,此后的函数体变为异步执行。
// First start --- First middle --- Second start --- Second middle --- First end --- Second end
// 对应的Promise 写法
function foo(name) {
return new Promise((resolve) => {
console.log(name, "start")
resolve(console.log(name, "middle"))
}).then(() => {
console.log(name, "end")
})
}
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)
// 输出:1 2 3 4 5 8 9 6 7
错误处理
如果await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被reject
。
async function f() {
await new Promise(function(resolve, reject) {
throw new Error('出错了')
})
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
async
函数f
执行后,await
后面的 Promise 对象会抛出一个错误对象,导致catch
方法的回调函数被调用,它的参数就是抛出的错误对象。
防止出错的方法,也是将其放在try...catch
代码块中。
async function f() {
try {
await new Promise(function(resolve, reject) {
throw new Error('出错了')
})
} catch(e) {}
return await('hello world')
}
如果有多个await
命令,可以统一放在try...catch
结构中。
async function main() {
try {
const val1 = await firstStep()
const val2 = await secondStep()
const val3 = await thirdStep()
console.log('Final:', val3)
} catch(err) {
console.log(err)
}
}
const superagent = require('superagent')
const NUM_RETRIES = 3
async function test() {
let i
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error')
break
} catch(error) {}
}
console.log(i)
}
test()
如果await
操作成功,就会使用break
语句退出循环;如果失败,会被catch
语句捕捉,然后进入下一轮循环。
使用注意
1、await 命令后面的Promise 对象,运行结果可能是rejected,最好把await 命令放在try...catch 代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise()
} catch(error) {
console.log(err)
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function(err) {
console.log(err)
})
}
2、多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
3、await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
4、与forEach
使用需小心:forEach()
方法要求使用同步函数——它不会等待 promise 执行完成。
Using Babel will transform async
/await
to generator function and using forEach
means that each iteration has an individual generator function, which has nothing to do with the others. so they will be executed independently and has no context of next()
with others. Actually, a simple for()
loop also works because the iterations are also in one single generator function.
function dbFuc(db) { // 这里不需要async
let docs = [{}, {}, {}]
// 可能得到错误结果
docs.forEach(async function(doc) {
await db.post(doc)
})
}
// 法一:for ... of
async function dbFuc(db) {
let docs = [{}, {}, {}]
for (let doc of docs) {
await db.post(doc)
}
}
// 法二
// reduce() 方法的第一个参数是async函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用await等待它操作结束。
// reduce()方法返回的是docs 数组最后一个成员的async 函数的执行结果,也是一个Promise 对象,在它前面也必须加上await。
async function dbFuc(db) {
let docs = [{}, {}, {}]
await docs.reduce(async (_, doc) => {
await _
await db.post(doc)
}, undefined)
}
5、async 函数可以保留运行堆栈。
const a = async () => {
await b()
c()
}
b()
运行的时候,a()
是暂停执行,上下文环境都保存着。一旦b()
或c()
报错,错误堆栈将包括a()
。
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
// 所有的async函数都可以写成以下形式,其中的spawn 函数就是自动执行器。
function fn(args) {
return spawn(function* () {
// ...
}
}
// spawn 函数的实现
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF()
function step(nextF) {
let next
try {
next = nextF()
} catch(e) {
return reject(e)
}
if (next.done) {
return resolve(next.value)
}
Promise.resolve(next.value).then(function(v) {
step(function() {
return gen.next(v)
})
}, function(e) {
step(function() { return gen.throw(e) })
})
}
step(function() { return gen.next(undefined) })
})
}
实例场景
按顺序完成异步操作
如依次远程读取一组 URL,然后按照读取的顺序输出结果。
function logInOrder(urls) {
// 远程读取所有URL
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text())
})
// reduce - 按次序输出
// reduce 方法依次处理每个 Promise 对象,然后使用then 将所有 Promise 对象连起来,因此就可以依次输出结果
// 写法不太直观,可读性比较差
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => chonsole.log(text))
}, Promise.resolve())
// 或者for of -按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise)
}
}
forEach 改造 - 同步化
Array.prototype.myForEach = async function(callback, thisArg) {
const _arr = this
const _isArray = Array.isArray(_arr)
const _thisArg = thisArg ? Object(thisArg) : window
if (!_isArray) {
throw new TypeError('The caller of myForEach must be the type "Array"')
}
for (let i = 0; i < _arr.length; i++) {
await callback.call(_thisArg, _arr[i], i, _arr)
}
}
fun([
() => console.log('start'),
() => sleep(1000),
() => console.log('1'),
() => sleep(2000),
() => console.log('2'),
() => sleep(3000),
() => console.log('end')
])
function sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function fun(arr) {
arr.myForEach(async (fn) => {
await fn()
})
}
运动路径动画
移动页面上的元素target,先从原点出发,向左移动20px,再向上移动50px,最后再向左移动30px,请把运动路径动画实现出来。
将移动的过程封装成一个walk 函数,该函数要接受以下3个参数。
- direction:字符串,表示移动方向,这里简化为“left” “top” 两种枚举。
- distance:整形,移动距离
- callback:执行动作后的回调
// 回调
const target = document.querySelectorAll('#main')[0]
target.style.cssText = `position: absolute;left: 0;top: 0`
const walk = (direction, distance, callback) => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束,执行下一个回调
callback && callback()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
walk(direction, distance, callback)
}
}, 20)
}
walk('left', 20, () => {
walk('top', 50, () => {
walk('left', 30, Function.prototype)
})
})
// Promise
const target = document.querySelectorAll('#main')[0]
target.style.cssText = `position: absolute;left: 0;top: 0`
const walk = (direction, distance) => {
new Promise((resolve, reject) => {
const innerWalk = () => {
setTimeout(() => {
let currentLeft = parseInt(target.style.left, 10)
let currentTop = parseInt(target.style.top, 10)
const shouldFinish = (direction === 'left' && currentLeft === -distance) || (direction === 'top' && currentTop === -distance)
if (shouldFinish) {
// 任务执行结束,执行下一个回调
resolve()
} else {
if (direction === 'left') {
currentLeft--
target.style.left = `${currentLeft}px`
} else if (direction === 'top') {
currentTop--
target.style.top = `${currentTop}px`
}
innerWalk()
}
}, 20)
}
innerWalk()
})
}
walk('left', 20).then(() => walk('top', 50)).then(() => walk('left', 30))
// walk 函数不再嵌套调用,不再执行callback,而是整体返回一个Promise,以便控制和执行后续任务
// 设置innerWalk 对每个像素进行递归调用
// 在当前任务结束时(shouldFinish 为true),对当前Promise 进行决议
// Generator 方案
// walk 函数同上
function *taskGenerator() {
yield walk('left', 20)
yield walk('top', 50)
yield walk('left', 30)
}
const gen = taskGenerator()
// 在以上代码中,定义了一个taskGenerator 生成函数,并实例化出gen。在此基础上,可以手动执行gen.next(),使
// 目标向左偏移20px,再次手动执行gen.next() 会使目标物体向上偏移50px,然后手动执行gen.next() 使目标向左偏移30px
// 唯一不便之处就是需要我们反复手动执行gen.next()
// kj 大神的co 库能够自动包裹Generator 并执行
// async/await
// walk 函数同上
const task = async function() {
await walk('left', 20)
await walk('top', 50)
await walk('left', 30)
}
红绿灯任务控制
红灯3s 亮一次,绿灯1s 亮一次,黄灯2s 亮一次,如何让3个不断交替重复地亮呢?
const taskRunner = async () => {
await task(3000, 'red')
await task(1000, 'green')
await task(2000, 'yellow')
taskRunner()
}