Appearance
async 函数
ES2017 - async 函数:使异步操作变得更加方便,Generator 函数语法糖
Generator 读取两个文件
js
const fs = require('fs')
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error)
resolve(data)
})
})
}
const gen = function* () {
const f1 = yield readFile('/etc/fstab')
const f2 = yield readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}改写为 async
写法差异:async 函数将星号(*)替换成 async,将 yield 替换成 await
js
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab')
const f2 = await readFile('/etc/shells')
console.log(f1.toString())
console.log(f2.toString())
}async 关键字
async 优势
内置执行器。Generator 函数执行必须靠执行器,而 async 函数自带执行器
更好的语义。async 和 await,比起星号和 yield,语义更清楚。async 表示异步操作,await 表示等待
更广的适用性。yield 命令后面只能是 Thunk 函数或 Promise 对象,而 await 命令后面,可以是 Promise 对象和原始类型的值
返回值是 Promise:async 函数的返回值是 Promise 对象,Generator 返回值为 iterator 对象
async 命令
async 关键字,表明函数内部有异步操作。调用该函数时,会立即返回一个 Promise 对象
js
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name)
const stockPrice = await getStockPrice(symbol)
return stockPrice
}
getStockPriceByName('goog').then(function (result) {
console.log(result)
})async 函数返回的是 Promise 对象,所以可以作为 await 参数
js
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('hello world', 50)async 函数有多种使用形式
js
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态,错误会被 catch 方法回调接收
js
async function f() {
throw new Error('出错了')
}
f().then(
(v) => console.log('resolve', v),
(e) => console.log('reject', e)
)async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。
js
async function getTitle(url) {
let response = await fetch(url)
let html = await response.text()
return html.match(/<title>([\s\S]+)<\/title>/i)[1]
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"getTitle 中三个操作全部完成后,才执行 then
await 命令
await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值
js
async function f() {
// 等同于
// return 123;
return await 123
}
f().then((v) => console.log(v))
// 123await 命令后面是一个 thenable 对象(即定义了 then 方法的对象)
js
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例子:实现 sleep
js
function sleep(interval) {
return new Promise((resolve) => {
setTimeout(resolve, interval)
})
}
async function start() {
await sleep(1000)
}
start()await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到
js
async function f() {
await Promise.reject('出错了')
}
f()
.then((v) => console.log(v))
.catch((e) => console.log(e))
// 出错了注意,抛出的错误会改变 async 函数 返回的 Promise 状态,所以不需要 return 也会被外部捕获。
await 后面 Promise 变为 reject 状态,会中断整个 async 函数
js
async function f() {
await Promise.reject('出错了')
await Promise.resolve('hello world') // 不会执行
}前一个异步错误,不中断后面的解决方案
- 通过 try...catch 捕获错误
js
async function f() {
try {
await Promise.reject('出错了')
} catch (e) {}
return await Promise.resolve('hello world')
}- Promise 后面 添加 catch 方法
js
async function f() {
await Promise.reject('出错了').catch((e) => console.log(e))
return await Promise.resolve('hello world')
}await 后面的异步操作抛出错误,等同于 async 返回 Promise 被 reject
js
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了')
})
}
f()
.then((v) => console.log(v))
.catch((e) => console.log(e))防止出错的方法,也是将其放在 try...catch 代码块之中
应用:通过 try...catch 重复请求
js
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 (err) {}
}
console.log(i) // 3
}
test()注意点
await 后面 Promise 会抛出异常,所以 await 通常用 try...catch 包裹
多个 await 异步操作
js
function asyncFunc(v, t) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(v)
}, t * 1000)
})
}
const list = [
['first', 3],
['second', 2],
['three', 1]
]并发执行
js
// 1.Promise.all
async function runAsync1_1() {
const promiseQueue = list.map(([x, y]) => asyncFunc(x, y))
const res = await Promise.all(promiseQueue)
console.log('res: ', res)
}
// runAsync1_1()
// 2.手动
async function runAsync1_2() {
const p1 = asyncFunc(list[0][0], list[0][1])
const p2 = asyncFunc(list[1][0], list[1][1])
const p3 = asyncFunc(list[2][0], list[2][1])
const result1 = await p1
const result2 = await p2
const result3 = await p3
console.log('res :', [result1, result2, result3])
}
// runAsync1_2()继发执行
js
// 1. reduce
async function runAsync2_1() {
const res = []
await list.reduce(async (pre, cur) => {
// 保证执行顺序
await pre
const result = await asyncFunc(cur[0], cur[1])
res.push(result)
}, undefined)
console.log('res', res)
}
// runAsync2_1()
// 2. for...of
async function runAsync2_2() {
const res = []
for (const [x, y] of list) {
const result = await asyncFunc(x, y)
res.push(result)
}
console.log('res', res)
}
runAsync2_2()await 命令只能用在 async 函数之中,如果用在普通函数,就会报错
async 函数可以保留运行堆栈。
js
const a = () => {
b().then(() => c())
}上述代码,b 运行时, a 已经结束,b 所在上下文栈已经消失
js
const a = async () => {
await b()
c()
}async 会通过 await 暂停执行,所以上下文还在
async 实现原理
将 Generator 函数和自动执行器,包装在一个函数里
js
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
})
}spawn 函数就是自动执行器
js
// spawn 函数
function spawn(gen) {
const it = gen()
const next = (data) => {
const result = it.next(data)
if (result.done) return result.value
result.value.then((res) => {
next(res)
})
}
next()
}js
function mockFn() {
return spawn(function* () {
const res1 = yield asyncFunc('fn1', 1000)
const res2 = yield asyncFunc('fn2', 2000)
const res3 = yield asyncFunc('fn3', 3000)
// ...
console.log(res1)
console.log(res2)
console.log(res3)
})
}spawn 函数 promise 版本
- 返回值 promise
- yield 后面可以是 Promise 或原始值
js
function spawnPromise(gen) {
return new Promise((resolve, reject) => {
const it = gen()
const next = (func) => {
let result
try {
result = func()
} catch (error) {
reject(error)
}
if (result.done) {
resolve(result.value)
}
// 保证原始值也有then方法
Promise.resolve(result.value)
.then((res) => {
next(() => it.next(res))
})
.catch((err) => {
next(() => it.thrown(err))
})
}
next(() => it.next(undefined))
})
}异步处理方法比较
场景:某个 DOM 元素,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
Promise 实现
优点:相比回调更清除
缺点:Promise API 偏多,操作语义不容易看出
js
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
let ret = null
// 新建一个空的Promise
let p = Promise.resolve()
// 使用then方法,添加所有动画
for (let anim of animations) {
p = p.then(function (val) {
ret = val
return anim(elem)
})
}
// 返回一个部署了错误捕捉机制的Promise
return p
.catch(function (e) {
/* 忽略错误,继续执行 */
})
.then(function () {
return ret
})
}Generator 函数的写法
优点:语义更清楚
缺点:需要执行器,yield 后面必须为 Promise
js
function chainAnimationsGenerator(elem, animations) {
return spawn(function* () {
let ret = null
try {
for (let anim of animations) {
ret = yield anim(elem)
}
} catch (e) {
/* 忽略错误,继续执行 */
}
return ret
})
}async 函数
优点:语义清除,无需执行器
js
async function chainAnimationsAsync(elem, animations) {
let ret = null
try {
for (let anim of animations) {
ret = await anim(elem)
}
} catch (e) {
/* 忽略错误,继续执行 */
}
return ret
}实战: 顺序执行异步操作
一组异步操作,需要按照顺序完成
Promise 写法:先将 Promise 存入数组,然后遍历数组
js
function logInOrder(urls) {
// 远程读取所有URL
const textPromises = urls.map((url) => {
return fetch(url).then((response) => response.text())
})
// 按次序输出
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text))
}, Promise.resolve())
}async 函数实现:通过 await 保证执行。相较 Promise 写法,需要前面请求完成,才能执行下一个,效率更低。
js
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url)
console.log(await response.text())
}
}优化:并发请求
js
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async (url) => {
const response = await fetch(url)
return response.text()
})
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise)
}
}顶层 await
早期的语法,await 命令只能出现在 async 函数内部,否则都会报错
ES2022:允许在模块的顶层独立使用 await 命令
js
// awaiting.js
let output
async function main() {
const dynamic = await import(someMission)
const data = await fetch(url)
output = someProcess(dynamic.default, data)
}
main()
export { output }js
// usage.js
import { output } from './awaiting.js'
function outputPlusValue(value) {
return output + value
}
console.log(outputPlusValue(100))
setTimeout(() => console.log(outputPlusValue(100)), 1000)问题:执行结果,完全取决于执行的时间。如果 awaiting.js 里面的异步操作没执行完,加载进来的 output 的值就是 undefined
解决:原始模块输出 Promise,通过 Promise 状态判断异步操作是否完成
js
// awaiting.js
let output
export default (async function main() {
const dynamic = await import(someMission)
const data = await fetch(url)
output = someProcess(dynamic.default, data)
})()
export { output }js
// usage.js
import promise, { output } from './awaiting.js'
function outputPlusValue(value) {
return output + value
}
promise.then(() => {
console.log(outputPlusValue(100))
setTimeout(() => console.log(outputPlusValue(100)), 1000)
})这个方案需要使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。新模块有对外输出,又要按照同样的规则
顶层的 await 命令就是为了解决这个问题,保证只有异步操作完成,模块才会输出值。
js
// awaiting.js
const dynamic = import(someMission)
const data = fetch(url)
export const output = someProcess((await dynamic).default, await data)注意:顶层 await 只能用在 ES6 模块,不能用在 CommonJS 模块。因为 CommonJS 模块的 require()是同步加载,如果有顶层 await,就没法处理加载
顶层 await 的一些使用场景
js
// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`)
// 数据库操作
const connection = await dbConnector()
// 依赖回滚
let jQuery
try {
jQuery = await import('https://cdn-a.com/jQuery')
} catch {
jQuery = await import('https://cdn-b.com/jQuery')
}多个包含顶层 await 命令的模块,加载命令是同步执行
js
// x.js
console.log('X1')
await new Promise((r) => setTimeout(r, 1000))
console.log('X2')
// y.js
console.log('Y')
// z.js
import './x.js'
import './y.js'
console.log('Z')
// X1、Y、X2、Z