使用Javascript如何优雅的编写异步代码
async / await 是一种与 Promise协作的特殊的语法糖。它使得我们可以像写同步代码一样书写异步代码。仅此而已
本文主要内容
- Callback / Promise / Generator / Async / Await
- 几个常见概念
- 同步循环
- 异步循环
一、Callback / Promise / Generator / Async / Await
Promise 是异步编程的一种解决方案
- 比传统的解决方案——回调函数和事件——更合理和更强大
- Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果
- 从语法上说,Promise 是一个对象,从它可以获取异步操作的消息
- Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
1.callback传统回调方法
function printStr(str, cb) {
setTimeout(() => {
console.log(str)
cb()
}, 1000)
}
function printAll() {
printStr('a', () => {
printStr('b', ()=> {
printStr('c', () => {
console.log('end')
})
})
})
}
printAll()
// a -> b -> c end [after: 1000ms] [total: 3000ms]
最大的问题是:回调地狱
2.Promise 方式
首先我们封装一个 promise 异步函数
function printStr(str){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(str)
resolve()
}, 1000)
})
}
然后调用then方法
then方法,可分别指定resolved状态和rejected状态的回调函数
// 此处只指定了 resolved 状态,rejected同理
// 语法:.then(()=>{/*resolved*/}, ()=>{/*rejected*/})
function printAll() {
printStr('a')
.then(()=> {
return printStr('b')
})
.then(() => {
return printStr('c')
})
}
// 可以进一步简化
// 去掉箭头函数的包装
// 等价于一下函数
function printAll() {
printStr('a')
.then(() => printStr('b'))
.then(() => printStr('c'))
}
printAll()
// a -> b -> c [after: 1000ms] [total: 3000ms]
Async / await函数是ES2017(ES8)的新增功能
- 让异步逻辑用同步写法实现
- 最底层的await返回需要是Promise对象
- 可以通过多层 async function 的同步写法代替传统的callback嵌套。
3.async/await 方式
async function printAll() {
await printStr('a')
await printStr('b')
await printStr('c')
}
printAll()
// a -> b -> c [after: 1000ms] [total: 3000ms]
4.在async/await出现之前还有一种过渡方式,Generator
【已经被async/await取代】
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,遍历器对象(Iterator Object)
function* printAll() {
yield printStr('a')
yield printStr('b')
}
const gen = printAll() // 调用后该函数并不执行, 返回指向内部状态的指针对象
gen.next() // [yield暂停执行 / next恢复执行]
二、几个常见概念
1.await只能在异步函数中使用
也就是 await 只能 包含在 async函数里:
async function printAll() {
await printStr()
}
2.async 可以单独使用
async function msg() {
return 'hello async await'
}
注意:async 函数总是返回一个promise,因此以下的结果可能不符合您预期:
const str = msg()
console.log(str)
// Promise {<resolved>: "hello async await"}
你应该这么操作:
msg().then((str)=> {
console.log(str)
})
3.两种书写形式
// 用于测试的 promise 方法
function getData(str) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(str)
}, 1000)
})
}
函数表达式:
const msg1 = async function() {
return await getData('str1')
}
msg1().then(text => console.log(text))
// str1
箭头函数表达式:
const msg2 = async() => {
return await getData('str2')
}
msg2().then(text => console.log(text))
// str2
4.异常处理 try…catch
// 测试 promise 函数
function yayOrNay() {
return new Promise((resolve,reject)=>{
// 0 or 1, at random
const val = Math.round(Math.random() * 1);
val ? resolve('Lucky!!') : reject('Nope ?');
});
}
将 await 包裹在 try…catch中:
async function msg() {
try {
const msg = await yayOrNay();
// await xxx
// await xxx
// 当存在多个 await 的时候也可以同时包裹在一个try catch 里面
console.log(msg);
} catch (err) {
console.log(err);
}
}
msg()
msg()
msg()
msg()
msg()
msg()
/*
2 Lucky!!
Nope ?
Lucky!!
2 Nope ?
*/
async 函数总是返回一个promise,所以我们可以直接调用catch 进行异常处理:
async function msg() {
const msg = await yayOrNay();
console.log(msg);
}
msg().catch(x => console.log(x));
5.Promise几个重要API
需要明确一点:async / await是 Promise 的补充而非取代,让我们写异步代码更趋向同步。Promise还有非常强大的API,用于处理不同场景
场景:同时发送多个ajax请求
Promise.all
返回一个Promise实例
Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例
成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,失败的时候则返回最先被reject失败状态的值
Promise.all 返回的结果和传入的 promise对象顺序是一致的。例如:Promise.all( [ajax1, ajax2]),ajax1执行时间长于ajax2的时间,但返回结果依然保持顺序,result = [resAjax1, resAjax2]
function printAll() {
// 等价于 Promise.all(['a', 'b', 'c'].map(str => printStr(str)))
return Promise.all([printStr('a'), printStr('b'), printStr('c')])
}
printAll().then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
// a -> b -> c [after: 1000ms] [total: 1000ms+ ]
Promise.race
返回一个最先执行完的 promise
一旦迭代器中的某个promise解决或拒绝,返回的promise就会解决或拒绝。也就是最先执行完的ajax请求
function printRace() {
return Promise.race(['a', 'b'].map(str => printStr(str)))
}
printRace().then((result) => {
console.log(result)
}).catch((error) => {
console.log(error)
})
// a or b [total: 1000ms]
当然 Promise 还有 Promise.resolve()、Promise.reject()等API,但都是为了让我们在异步编程过程中更加方便
三、同步循环
同步循环是我们每天写代码过程中都会使用的,非常基础
主要是为了和异步循环做对比
function syncFun() {
const arr = [1, 2, 3]
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i])
}
// or map/forEach
/*
arr.map((number) => {
console.log(number)
})
*/
}
syncFun()
// 1 2 3
四、异步循环
参考:JavaScript loops - how to handle async/await
定义一个异步函数 promise:
function asyncFun(number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(number)
resolve()
}, 1000)
})
}
在for 循环中使用 await
map、forEach 等这些高阶循环方法,循环体里面都是异步的。需要等后续操作完成,才执行循环体中的方法
所以此处输出结果不符合我们预期:
async function test() {
console.log('start')
const arr = [1, 2, 3]
arr.map(async(number) => {
await asyncFun(number)
})
console.log('end')
}
// start end 1 2 3
// [total: 1000ms+]
串行遍历
我们放弃使用高阶循环,使用原生循环方法,可以解决这个问题
async function test() {
console.log('start')
const arr = [1, 2, 3]
for (let i = 0, len = arr.length; i < len; i++ ) {
await asyncFun(arr[i])
}
console.log('end')
}
test()
// start 1 2 3 end
// [total: 3000ms]
并行遍历
如果需要让循环体里面的内容执行完了之后再执行后面的操作,我们可以用 Promise.all,实现串行输出:
async function test() {
console.log('start')
const arr = [1, 2, 3]
const promises = arr.map((number) => {
return asyncFun(number)
})
await Promise.all(promises)
console.log('end')
}
// start 1 2 3 end
// [total: 1000ms +]
注意:当数据量很大的时候,会有并行性能问题