Promise与async/await

异步回调函数与Promise

异步编程

实际开发中经常会遇到这样的需求:从A请求获取到的数据,得到的结果传给B请求,按照ajax的回调函数写法应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   $.ajax({ 
url: '',
dataType:'json',
success: function(data) {
// 获取data数据 传给下一个请求
var id = data.id;
$.ajax({
url:'',
data:{"id":id},
success:function(){ // .....

}
});
}
});
  • 以上请求存在的问题:

    • 1.请求之间依赖关系,造成ajax请求嵌套形成回调地狱,代码难以维护,可读性极差。
    • 2.错误处理的代码与正常的业务代码耦合在一起,造成代码非常难看。

由于以上原因,ES6中的Promise提供了这样的异步回调函数解决方案,先来看promise的兼容性情况:

promise-compatibility

关于Promise的创建及使用,在Promise应用场景与模块化尝试已经做了足够的描述,此处不再赘述。

Promise是解决异步回调的一种方式,每一个then方法都会返回一个新建的Promise对象,无论你的异步操作成功与否,这个对象都会返回一个值。Promise链写的代码,比层层调用回调函数更优雅,流程也更明确。先获得数据库对象,再获得集合对象,最后查询数据。

Promise存在的问题

每一个then()方法获取的对象,都是由上一个返回的数据,并且不可以跨层访问。在Node.js端mongodb js驱动默认返回Promise,以此使用Promise链为例:

1
2
3
4
5
6
7
8
9
10

MogoClient.connect(url + db_name).then( db =>{
return db.collection("blogs");
}).then(coll => {
return coll.find().toArray();
}).then(blogs => {
console.log(blogs.length);
}).catch(err => {
console.log(err);
})

第三个then方法只能获取到查询的结果blogs,而不能使用上级的db对象及coll对象。这时,如果需要打印出blogs列表后,关闭数据库db.closer()。而如果要达到这个目的,可以使用两个解决方案:

一,使用then()嵌套。将Promise链打断,使其嵌套,与回调函数的嵌套相同:

1
2
3
4
5
6
7
8
9
10
11
MongoClient.connect(url + db_name).then(db=> {
let coll = db.collection('blogs');
coll.find().toArray().then(blogs=> {
console.log(blogs.length);
db.close();
}).catch(err=> {
console.log(err);
});
}).catch(err=> {
console.log(err);
})

显然这并不是一种很好的解决办法,从ajax的回调地狱进入到了Promise回调地狱的泥沼,并且需要对每一个Promise捕捉异常,Promise并没有形成链。

二,在每个then()方法都将db传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MongoClient.connect(url + db_name).then(db=> {
return {
db:db,
coll:db.collection('blogs')
};
}).then(result=> {
return {
db:result.db,
blogs:result.coll.find().toArray()
};
}).then(result=> {
return result.blogs.then(blogs=> {
//注意这里,result.coll.find().toArray() 返回的是一个Promise,因此这里需要再解析一层
return {
db:result.db,
blogs:blogs
}
})
}).then(result=> {
console.log(result.blogs.length);
result.db.close();
}).catch(err=> {
console.log(err);
});

在以上的例子中可以看出,如果返回值是一个Promise值,那每一个Promise都需要在进行解析。{db:result.db,blogs:result.coll.find().toArray()}对象中,blogs是一个Promise值,因此需要先用then方法解析一层,再将同步值db和blogs返回。这里涉及到了Promise的嵌套,不过一个Promise只嵌套了一层then()。

从以上两种解决方法来看,如果一定要使用Promise链,都不是最优的方法。

async/await解决方案

如果使用async/await改写以上的代码:

1
2
3
4
5
6
7
8
9
(async function(){
let db = await MongoClient.connect(url + db_name);
let coll = db.collection("blogs");
let blogs = await coll.find().toArray();
console.log(blogs.length);
db.close();
})().catch(err=>{
console.log(err);
})

代码中的async表明函数是异步的,同时await表示要等待异步操作返回值,而async其实就是Promise的一个语法糖,通过async/await函数,异步请求可以变得更直观优雅,接下来继续深入了解ES中的新特性。

深入async/await

关于async/await函数的兼容性,目前来说,浏览器兼容程度较之Promise来说还不是很高:

async-compatibility

根据文档定义,async返回的就是一个Promise对象,所以在最外层不能使用await获取到其返回值时,可以使用then()解析这个Promise对象。如果async函数没有返回值,它返回的就是Promise.resolve(undefined)。联想到Promise的一个特点—立即执行,所以并不会阻塞之后的语句,这与普通返回的Promise对象并没有不同。

所以,还是在于await关键字上。那么,await等待的到底是什么呢?

await不仅等待Promise对象,可以等任何表达式的结果。 所以,await后可以是普通函数调用或直接量。如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   function sleep(second){
return "someThing";
}

async function testAsync(){
return new Promise((resolve,reject)=>{
resolve("hello Async");
}).then((v)=>{
return v;
})
}

async function test(){
const v1 = await sleep();
const v2 = await testAsync();
console.log(v1,v2);
}
test();

await等到了想要的东西,一个Promise对象或其他值,如果等到的是Promise对象,await就会阻塞后面的代码,等待Promise被解析完成得到resolve值,作为await表达式的运算结果。

关于阻塞,这就是await必须放在async函数中的原因。async函数调用不会造成阻塞,因为内部所有的阻塞都被封装在了Promise对象中异步执行。

async/await的优势

单一的Promise链并没有看出来async/await的优势,但是如果需要处理多个Promise形成的then链时,async/await函数的优点就能凸显无疑。

例如,有三个请求按时间顺序分别为A、B、C。B依赖于A的结果返回解构,C依赖于B的请求结果结构。如果使用回调函数会有3层回调,Promise则会有三个then,同样会是嵌套的Promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   function sleep(second, param) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(param);
}, second);
})
}

function doIt(){
let res1,res2,res3;
res1 = sleep(1000,"req01");
res2 = res1.then((v) => {
console.log(v);
sleep(5000,"req02"+v).then((v) => {
console.log(v);
res3 = sleep(500,"req03"+v).then((v) => {
console.log(v)
})
})
});
}
doIt();

而如果使用async/await改造该方法,从可读性和易维护上就提升了许多。

1
2
3
4
5
6
7
8
9
10
11
12
   //...sleep()
async function doIt(){
let res01 = await sleep(1000,"req01");
let res02 = await sleep(5000,"req02"+res01);
let res03 = await sleep(500,"req03"+res02);
console.log(`
${res03}
${res02}
${res01}
`)
}
doIt();

错误处理

因为async/await不再需要使用then()解析,因此也就不存在使用链式的catch捕捉错误,直接用try/catch包裹就能捕捉错误。

例如:

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('want to sleep~');
        }, second);
    })
}

async function errorDemo() {
    let result = await sleep(1000);
    console.log(result);
}
errorDemo();// VM706:11 Uncaught (in promise) want to sleep~

// 为了处理Promise.reject 的情况,应该将代码块用 try catch 包裹一下
async function errorDemoSuper() {
    try {
        let result = await sleep(1000);
        console.log(result);
    } catch (err) {
        console.log(err);
    }
}

并行

对于没有相互关联性的并发请求,并不需要将其放入await中阻塞请求进程。例如A、B、C三个异步请求结束之后清除加载动画,并不需要将以上三个需求放置在await中,而应该是这样:

function sleep(second) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('request done! ' + Math.random());
        }, second);
    })
}

async function Demo(){
    let p1 = sleep(1000);
    let p2 = sleep(1000);
    let p3 = sleep(2000);
    await Promise.all([p1,p2,p3]);
    console.log("clear the loading");
}
Demo();