前言
在 jQuery 盛行的时代,我们最难避免的,就是回调地狱,尤其是当遇到了嵌套 ajax 请求的时候,我们的代码大都是这样的:
$.ajax({
url:'xxx',
success:function(data){
$.ajax({
url:'yyy' + data.xxxx,
success:function(data){
$.ajax({
url:'zzz' + data.xxxx,
success:function(data){
//dosomething
}
})
}
})
}
})
这样的代码,一是嵌套深了看起来难看,而是不利于调试,三是当一个回调里面的逻辑太多了的时候,一个方法的代码可能很长很长。于是,聪明的你肯定这样做过:
function fn1(data){
$.ajax({
url:'yyy' + data.xxxx,
success:function(data){
fn2(data)
}
})
}
function fn2(data){
$.ajax({
url:'zzz' + data.xxxx,
success:function(data){
//dosomething
}
})
}
$.ajax({
url:'xxx',
success:function(data){
fn1(data)
}
})
这样虽然看上去没上面那么糟糕了,但又却暴露出另外一个问题,各个 ajax 之间的依赖关系不是那么的明朗,对于后期维护代码的人而言,这是个很痛苦的事情。
后来随着 Promise 的出现,我们的回调地狱稍微有了改观,变成了下面这样:
new Promise((resolve) => {
$.ajax({
url:'xxx',
success:function(data){
resolve(data)
}
})
}).then((data) => {
return new Promise((resolve) => {
$.ajax({
url:'yyy' + data.xxxx,
success:function(data){
resolve(data)
}
})
})
}).then((data) => {
$.ajax({
url:'zzz' + data.xxxx,
success:function(data){
// dosomething
}
})
})
但还是没达到最优雅的姿势,直到 async 和 await 的出现,我们的代码总算变得更加清晰明了,各种异步之间的依赖关系看得一清二楚:
const ajax1 = function(){
return new Promise((resolve,reject) => {
$.ajax({
url:'xxx',
success:function(data){
resolve(data)
}
})
// 你可以用setTimeout 代替ajax进行异步模拟
// setTimeout(function(){
// resolve('ajax1')
// },1000)
})
}
const ajax2 = function(data){
return new Promise((resolve,reject) => {
$.ajax({
url:'yyy' + data.xxxx,
success:function(data){
resolve(data)
}
})
})
}
const ajax3 = function(data){
return new Promise((resolve,reject) => {
$.ajax({
url:'zzz' + data.xxxx,
success:function(data){
resolve(data)
}
})
})
}
var fn = async function(){
let ajax1Data = await ajax1()
// ajax2 依赖ajax1的返回值
let ajax2Data = await ajax2(ajax1Data)
// ajax3 依赖ajax2的返回值
let ajax3Data = await ajax3(ajax2Data)
// 拿到了ajax3Data,做点别的
dosomething()
}
fn()
}
接下来,我们就来谈谈 async 和 await 的 api 和 使用注意事项。
async
- async 函数返回的是一个 Promise 对象,如果结果是值,会经过 Promise 包装返回。
- async 函数中,如果有多个 await 关键字时,如果有一个 await 的状态变成了 rejected,那么后面的操作都不会继续执行。
- 如果在一个 async 方法中,有多个 await 操作的时候,程序会变成完全的串行操作,后一个会一直等到前一个执行完成才会执行。如果你的业务场景是多个异步操作之间不存在结果的依赖关系,请使用 promise.all。
async 函数声明方式:
// 普通的函数声明
async function fn(){}
// 声明一个函数表达式
let fn = async function(){}
// async形式的箭头函数
let fn = async () => {}
await
- await 只能存在与 async 方法内部,在其他地方不行。
- await 只能在 async 函数的当前作用域下执行,不能跨层级使用。
- await 命令后面可以是 Promise 对象或值,如果是值,会自动转成一个立即 resolve 的 Promise 对象。
- await 的返回结果是它后面所跟的 promise 的执行的结果,可能是 resolved 或者 rejected 的值。
注意点
- 对于下面这段代码,打印结果并不是你期望的返回值 1,而是一个 promise。
function get(){
return 1
}
async function getData(){
let value = await get(); // 记住:await 后面的结果可以是值,也可以是 promise
value++;
return value;
}
var value = getData();
console.log(value)
因为 async 方法返回的永远是一个 promise,即使开发者返回的是一个常量,也会被自动调用 promise.resolve 方法转换为一个 promise。因此对于这种情况,上层调用方法也需要是 async 函数,所以你得像下面这样才能得到想要的结果:
(async function(){
var value = await getData()
console.log(value)
})();
- 如下代码也会报错,因为 await 和 async 中间跨了一层作用域。
async function fn1(){
console.log('fn1 start')
}
async function fn2(){
console.log('fn2 start')
function cross(){
await fn1()
}
cross()
}
fn2()
思考: 请看下面代码,并猜想一下执行结果的打印顺序。
async function fn1(){
console.log('fn1 start')
await fn2()
console.log('fn1 end')
}
async function fn2(){
console.log('fn2 start')
await fn3()
console.log('fn2 end')
}
async function fn3(){
console.log('fn3')
}
fn1();
console.log('over')
正确结果如下:
fn1 start
fn2 start
fn3
over
fn2 end
fn1 end
跟你自己猜想的答案一样吗?如果不一样,那你可能得看看下面这段分析了。
就上面的代码而言,因为我们一开始调用了 fn1,所以第一个打印 fn1 start 毋庸置疑,但因为要等待 fn2 执行完,所以不会马上执行后面的代码;
接着 fn1 又调用了 fn2,所以会立即执行 fn2,打印了fn2 start,同 fn1 一样,由于要等待 fn3,所以不会马上执行后面的代码;
接着 fn2 又调用了 fn3,所以打印了fn3;
这个时候,已经没有异步代码了,直接执行最后一行代码,打印 over;
最后一行代码执行过后,已经没有同步代码了,所以开始等待异步执行;
这个时候,不防先思考一下我们的异步队列有哪些,分别是两次通过 await 添加的 fn2 和 fn3;
所以在 console.log(‘fn1 end’) 执行之前,要等待 fn2,因为 fn3 里已经没有异步了,所以直接打印 fn2 end;
最后等到 fn2 执行完成,直接打印 fn1 end。
其实,就上面的示例而言,抛开最后一行代码,你会发现,这跟 koa 的中间件差不多是一个逻辑。这种剥洋葱的模型在前端很多,最典型的就是 dom 事件的捕捉与冒泡。
实用场景举例
上文提到的ajax嵌套就是一个典型的例子。
再比如,react 的 setState 方法,很多人是像这样使用的:
changeState(){
this.setState({
value: newValue
}, () => {
console.log(this.state.value)
})
}
用了 async 和 await 之后,就是下面这样:
async changeState(){
await this.setState({
value: newValue
}
console.log(this.state.value)
}
还有,如果你用 node 做过后端开发,当我们使用 mysql 时,可以这样封装我们的查询方法:
// 统一执行 sql 的函数
function exec(sql) {
const promise = new Promise((resolve, reject) => {
// con 是我们的链接对象
con.query(sql, (err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
})
})
return promise
}
module.exports = {
exec,
escape: mysql.escape
}
使用的时候就可以用 async 和 await 了:
const { exec } = require('./db')
// 这里的 async 返回了 promise
const getList = async (author, keyword) => {
let sql = `select * from blogs where 1=1 `
if (author) {
sql += `and author='${author}' `
}
if (keyword) {
sql += `and title like '%${keyword}%' `
}
sql += `order by createtime desc;`
return await exec(sql)
}
getList.then(res=>{
console.log(JSON.stringify(res))
})
OK,本文到此结束。附个人博客原文链接