前言
异步编程允许我们在执行一个长时间任务时,程序不需要进行等待,而是继续执行之后的代码,知道这些任务完成之后再回来通知你,通常是以回调函数(callback)的形式,这种编程模式避免了程序的阻塞,大大提高了CPU的执行效率,尤其适用于IO密集的,例如需要经常进行网络操作数据库访问的应用。
如果大家还不是很清楚并发、并行、异步、同步的概念,可以先去看看我之前的博客
并发&并行&同步&异步的区别
一、回调函数
我们知道在javascript中有两种实现异步的方式,首先第一种是传统的回调函数,比如我们可以使用setTimeout() 让一个函数在指定的时间后执行
setTimeout(()=>{
console.log("hello!");
},3000);
console.log("can you see me?");
这个函数本身会立刻返回,程序会紧接着执行之后的代码,而我们传入的回调函数则会等到预定的时间才会执行。
需要注意的是 JavaScript 从设计之初就是一个单线程的编程语言,即便看上去这里的回调函数和主函数在并发执行,但它们都运行在同一个主线程中。实际上主线程中还运行着我们写的其他代码,包括界面逻辑、网络请求、数据处理等。 虽然只有单个线程在执行,但这种单线程的异步编程方式其实有诸多优点。由于所有的操作都运行在同一个线程中,因此我们无需考虑线程同步或者资源竞争的问题,并且从源头上避免了线程之间的频繁切换,从而降低了线程自身的开销。
回调函数虽然简单好理解,但它有一个明显的缺点,如果我们需要依次执行多个异步操作,我们的程序可能会写成这样:
setTimeout(()=>{
console.log('先等三秒');
setTimeout(()=>{
console.log('再等三秒');
setTimeout(()=>{
console.log('又等三秒');
},3000);
},3000);
},3000);
当第一个任务执行完毕以后,在回调函数里面再去执行第二个任务,然后是第三个、第四个…整个程序会一层接着一层的嵌套下去,可读性会变得非常差,这种情况也被叫做函数的“回调地狱”。为了解决这个问题,Promise 应运而生。
二、Promise
JavaScript 中使用 Promise 的 API ,fetch() 就是一个很好的例子,它用来发起一个请求来获取服务器数据,我们可以用它动态更新页面的内容,也就是我们平时说的 Ajax 技术,这里我调用 fetch() 去访问一个测试地址的数据:
fetch 会立刻返回返回一个 Promise 对象
这里的 Promise 几乎就是它的字面意思,它代表一个“承诺”,“承诺”这个请求会在未来某个时刻返回数据,我们随后可以调用它的 then 方法并传递一个回调函数
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response)=>{
});
如果这个请求在未来成功完成,那么回调函数会被调起,请求的结果也会以参数的形式传递进来。
如果只是这样,Promise 和回调函数就没有什么区别了。其实 Promise 的优点在于 它可以用一种链式结构将多个异步操作串联起来
比如这里的 response.json() 方法也会返回一个 Promise
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json);
它代表在未来的某个时刻,将返回的数据转换成 JSON 格式,如果我们想等到它完成之后再执行其他的操作,我们可以在后面再追加一个 then 然后执行接下来的代码,比如将结果打印出来 Promise 的链式调用避免了代码的层层嵌套,即便我们有一个很长的链,代码也不过是向下方增长而非向右,因此可读性会提升很多
三、错误处理
在使用异步操作的时候,我们也可能遇到错误,比如各种网络问题或者返回的数据格式不正确等等 如果我们要捕获这些错误,最简单的方法是附加一个catch在链式结构的末尾,如果之前任意一个阶段发生了错误,那么catch将会被触发,而之后的 then() 将不会执行,这和同步编程中用到的 try/catch 块很类似
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((json) => {
console.log(json);
})
.catch((error => {
console.log(error);
}));
类似的,Promise 还提供 finally 方法,他会在 Promise 链结束之后调用,无论失败与否,我们可以在这里做一些清理工作
fetch("https://jsonplaceholder.typicode.com/posts")
.then((response) => response.json())
.then((json) => {
console.log(json);
})
.catch((error => {
console.log(error);
})
.finally(() =>{
}));
四、async/await
简单来说, async/await 是基于 Promise 之上的一个语法糖,可以让异步操作更加的简单明了。 首先我们需要使用 async 关键字将函数标记为异步函数(异步函数就是返回值为 Promise 对象的函数,比如之前用到的 fetch() 就是一个异步函数)。在异步函数中,我们可以调用其他的异步函数,不过我们不再需要使用 then() ,而是使用一个更加简洁的 await 语法,await 会等待 Promise 完成之后直接返回最终的结果
async function f(){
const response = await fetch("https://...");
}
f();
所以这里的 response 已经是服务器返回的响应数据了。 需要注意的是,await 虽然看上起会暂停函数的执行,但在等待的过程中,JavaScript 同样可以处理其他的任务,比如更新界面、运行其他程序代码等等。这是因为await底层是基于 Promise 和时间循环机制实现的。
await使用时的陷阱
最后,我们在使用 await 的时候需要留意一下几个陷阱
1
比如在这个例子中,如果我们分别去 await 这两个异步操作,虽然不存在逻辑错误,但这样写会打破这两个 fetch() 操作的并行,因为我们会等到第一个任务执行完成之后才开始执行第二个任务
async function f(){
const a = await fetch("https://.../post/1");
const b = await fetch("https://.../post/2");
}
这里更高效的做法是将所有 Promise 用 Promise.all 组合起来,然后再去 await
async function f(){
const promiseA = fetch("https://.../post/1");
const promiseB = fetch("https://.../post/2");
const [a,b] = await Promise.all([promiseA,promiseB]);
}
修改后的程序运行效率也会直接提升一倍。
2
如果我们需要在循环中执行异步操作,是不能够直接调用 forEach 或者 map 这一类方法的 尽管我们在回调函数中写了 await ,但这里的 forEach 会立刻返回,它并不会等到所有的异步操作都执行完毕。如果我们希望等待循环中的异步操作都一一完成之后才继续执行,那我们还是应该使用传统的 for 循环
async function f(){
for (let i of [1,2,3]) {
await someAsyncOperation();
};
console.log("done");
}
f();
更进一步,如果我们想要循环中的所有操作都并发执行,一种更炫酷的写法是使用 for await ,这里的for 循环依然会等到所有的异步操作都完成之后才继续向后执行
async function f(){
const Promise = [
someAsyncOperation(),
someAsyncOperation(),
someAsyncOperation(),
];
for await (let result of promises) {
}
console.log("done");
}
f();
3
我们不能再全局或者普通函数中直接使用 await 关键字,await 只能被用在异步函数(async function)中,如果我们想在最为层中使用 await 那么需要先定义一个异步函数,然后在函数体中使用它。
总结
使用 async/await 可以让我们写出更清晰、更容易理解的异步代码,有了它们之后,我们不再需要使用底层的 Promise 对象,包括调用它的 then(),catch() 函数等,即便是对于某些旧版本的浏览器,它们不支持 async 语法,我们还是可以使用转译器将它们编译成旧版本也兼容的等效代码。
|