在jQuery盛行的时代,我们最难避免的,就是回调地狱,尤其是当遇到了嵌套ajax请求的时候,我们的代码大都是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$.ajax({ url:'xxx', success:function(data){ $.ajax({ url:'yyy' + data.xxxx, success:function(data){ $.ajax({ url:'zzz' + data.xxxx, success:function(data){ //dosomething } }) } }) } }) |
这样的代码,一是嵌套深了看起来难看,而是不利于调试,三是当一个回调里面的逻辑太多了的时候,一个方法的代码可能很长很长。于是,聪明的你肯定这样做过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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的出现,我们的回调地狱稍微有了改观,但还是不够优雅:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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,代码看起来就更加清晰明朗了:
1 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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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
1、async 函数返回的是一个 Promise 对象,如果结果是值,会经过 Promise 包装返回。
2、async 函数中,如果有多个 await 关键字时,如果有一个 await 的状态变成了 rejected,那么后面的操作都不会继续执行。
3、如果在一个 async 方法中,有多个 await 操作的时候,程序会变成完全的串行操作,后一个会一直等到前一个执行完成才会执行。如果你的业务场景是多个异步操作之间不存在结果的依赖关系,请使用 promise.all。
async 函数声明
1 2 3 4 5 6 7 8 |
// 普通的函数声明 async function fn(){} // 声明一个函数表达式 let fn = async function(){} // async形式的箭头函数 let fn = async () => {} |
二、await
1、await 只能存在与 async 方法内部,在其他地方不行。
2、await 只能在 async 函数的当前作用域下执行,不能跨层级使用。
3、await 命令后面可以是 Promise 对象或值,如果是值,会自动转成一个立即 resolve 的 Promise 对象。
4、await 的返回结果是它后面所跟的 promise 的执行的结果,可能是 resolved 或者 rejected 的值。
# 注意点
1. 对于下面这段代码,打印结果并不是你期望的返回值 1,而是一个 promise。
1 2 3 4 5 6 7 8 9 10 11 12 |
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 函数,所以你得像下面这样才能得到想要的结果:
1 2 3 4 |
(async function(){ var value = await getData() console.log(value) })(); |
2. 如下代码也会报错,因为 await 和 async 中间跨了一层作用域。
1 2 3 4 5 6 7 8 9 10 11 |
async function fn1(){ console.log('fn1 start') } async function fn2(){ console.log('fn2 start') function cross(){ await fn1() } cross() } fn2() |
# 思考
请看下面代码,并猜想一下执行结果的打印顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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') |
正确结果如下,你的思考正确了吗?
1 2 3 4 5 6 |
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 方法,很多人是像这样使用的:
1 2 3 4 5 6 7 |
changeState(){ this.setState({ value: newValue }, () => { console.log(this.state.value) }) } |
用了 async 和 await 之后,就是下面这样:
1 2 3 4 5 6 |
async changeState(){ await this.setState({ value: newValue } console.log(this.state.value) } |
还有,当我们使用 mysql 时,可以这样封装我们的查询方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 统一执行 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 了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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,本文到此结束。
如有错误,欢迎指正