本文转载自微信公众号「零零后程序员小三」,作者003 。转载本文请联系零零后程序员小三公众号。
什么是异步编程?
异步编程允许我们在执行一个长时间任务的时候,程序不用进行等待就可以继续执行后面的代码,直到任务完成后再以回调函数(callback)的方式回头通知你
这种编程模式避免了程序的阻塞,提高了效率,它适用于那些网络请求或者数据库操作的应用。
实现异步的方式
回调函数,是最简单的实现异步的方式
console.log('111');
setTimeout(() => {
console.log("222");
}, 2000)
console.log('333');
- 1.
- 2.
- 3.
- 4.
- 5.
虽然,按照html文档的输出规则他是自上而下,但是在中间加了一个定时器,然后浏览器识别到了它,会马上执行,然后执行后面的代码,等到了给定时间,会以回调函数的方式返回。
在JS的设计之初,他一开始就是单线程的编程语言,尽管这里回调函数看上去和主线程一起进行的,但是都运行在一个线程中,况且主线程还运行其他代码。
虽然JS只有一个线程,但是还是有比较不错的优点的。因为所有的操作都在一个线程之中,所以不用去考虑资源竞争的问题,而且在源头就避开了线程之间的频繁切换,从而降低了线程开销
但是它也有一个致命的缺点,如果我们需要进行多个异步操作,我们可能会写出下面的代码
console.log("111");
setTimeout(() => {
console.log("三秒后执行1");
setTimeout(() => {
console.log("三秒后执行2");
setTimeout(() => {
console.log("三秒后执行3");
setTimeout(() => {
console.log("三秒后执行4");
}, 3000)
}, 3000);
}, 3000);
}, 3000);
console.log("333");
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
如果再有别的回调,这样会更恐怖,一直写下去,换谁都看得心慌。我们管这个叫回调地狱
解决回调地狱
为了解决这个回调地狱,Promise诞生了。
我们在页面中动态的更新数据,也就是AJAX技术,就是使用Promise的API fetch()实现的
我们可以试一试用fetch()获取一个接口的数据
通过运行可知道他返回的是一个Promise对象,但是我们还没有获得我们想要的数据,因为Promise翻译一下就是承诺的意思,所以,他应该会在后来给我们实现我们想要的需求,所以,我在后面加个then,then翻译一下就也是然后的意思
所以就是传入它的then方法并传入一个回调函数,如果在后来这个请求成功之后,然后回调函数会被调起,请求的函数会被作为一个参数传入
fetch("http://jsonplaceholder.typicode.com/posts/1")
.then((response)=> ...)
- 1.
- 2.
但是如果这样看来Promise和回调函数就没有区别了。
但是,Promise的优点在于它可以用一种链式结构将多个异步操作串联起来
也就是 比如下面的response.json()方法也会返回一个Promise,然后then之后就是将未来返回的response转换为json格式,
fetch("http://jsonplaceholder.typicode.com/posts/1")
.then((response)=>response.json())
- 1.
- 2.
然后我们还可以继续追加我们想要进行的操作,直接then下去,比如下面这样把结果打印出来或者把结果存到某个容器中等
fetch("http://jsonplaceholder.typicode.com/posts/1")
.then((response)=>response.json())
.then((json) => console.log(json))
- 1.
- 2.
- 3.
Promise的链式调用避免了代码层层嵌套,尽管有很长的链式调用,但也只是将代码向下方增长而不是向右。可读性会大大提高。
但是在使用异步操作的时候也会遇到错误,比如各种网络问题以及数据格式不正确等。然后我们可以通过在末尾添加一个catch()来捕获这些错误,如果之前任意一个阶段发生了错误,那么catch会被触发,然后之后的then将不会再执行
这跟同步编程中用到的try/catch块相似,Promise还提供了finally方法,会在Promise链结束后调用,无论是否出现错误,我们都可以在这里做函数清理的工作。毕竟要有首有尾嘛。
async/await
现在来看一下async/await,简单来说就是基于Promise之上的语法糖,可以让异步操作更加简单明了。
具体步骤就是
首先使用async将返回值为Promise对象的函数标记为异步函数,就像是刚刚用到的fetch()就是一个异步函数,在异步函数中可以调用其他异步函数,不过不是使用then,而是用更简单的await,中文意思就是等到,等待,所以await会等待Promise完成之后直接返回最终结果
用了async/await之后就是将函数变成异步函数,可以直接获取到我们想要的结果,所以some已经是服务器返回的响应数据了,然后我们就可以进行响应的操作了
await虽然看上去会暂停函数的执行,但是在等待的过程中同样可以处理其他任务,比如我这里将返回的数据转换为json数据,因为await底层就是基于Promise和事件循环机制实现的,具体操作还有很多自行去尝试。
这样我们就拿到了我们想要的数据了。
但是使用的时候也要留意await的错误用法
比如我这样
async function fn() {
const some1 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
const some2 = await fetch("http://jsonplaceholder.typicode.com/posts/2")
const some3 = await fetch("http://jsonplaceholder.typicode.com/posts/3")
...
}
fn()
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
虽然看起来没有什么错误啊,但是这样写会打破这两个fetch()操作的并行,因为我们是等到第一个任务完成再执行第二个任务,然后再执行后面的代码。
所以我们有个小妙招。
就是将所有Promise用Promise.all组合起来,然后再去await,比如我下面的做法
async function fn() {
const some1 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
const some2 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
const some3 = await fetch("http://jsonplaceholder.typicode.com/posts/1")
const [a,b,c] = await Promise.all([some1,some2,some3])
}
fn()
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
这样的做法会让运行程序效率提升很多。
最后,我们不能在全局或者普通函数中直接使用await关键字,await只在异步中有效,如果我们想要在最外层中使用await那么需要先定义一个异步函数,然后再在函数体中使用它
使用async await可以写更清晰更容易的理解异步代码,而且不用再使用底层的Promise对象,包括then(),catch()函数等
如果旧版本浏览器不支持async await语法,可以通过转译器编译成旧版本也兼容的代码