为什么使用异步编程?
JS在浏览器中的运行是单线程,当前面的任务没有完成的时候,后面的任务会一直在等待,这也造成了浏览器的“假死”现象。为了避免这一现象的发生,浏览器采用异步的方式来解决这一问题。
那么异步编程的方式有哪些呢?
回调函数
最早实现异步编程的方式就是回调函数,例如定时器:
function callback1()
{
setTimeout(()=>{
console.log("callback1");
},1000);
}
console.log(1);
callback1();
回调函数实现起来很简单,但是假如我有一个场景,这个场景是我要通过ajax请求去获取到用户数据,再根据用户数据获取到订单数据,最后根据订单数据来获取到商品数据(此处的Ajax请求利用定时器来进行模拟),我们可能会写出下面的代码:
function getMessage()
{
setTimeout(()=>{
let data="用户数据";
console.log(data);
setTimeout(()=>{
let data="订单数据";
console.log(data);
setTimeout(()=>{
let data="商品数据";
console.log(data);
},3000);
},2000);
},3000);
}
getMessage();
我们可以看到我们写出来的代码,比如getMessage会产生回调函数嵌套着回调函数,这种现象我们称它为回调地狱。回调地狱不利于代码的阅读和维护。那我们应该如何对他进行改进呢?
Promise
Promise解决了回调地狱的问题,采用链式的写法进行操作。将上面的代码改写为:
let p=new Promise((resolve,reject)=>{
setTimeout(()=>{
let data="用户数据";
resolve(data);
},3000);
}).then((res)=>{
console.log(res);
return new Promise((resolve,reject)=>{
setTimeout(()=>{
let data="订单数据";
resolve(data);
},3000)
})}).then((res)=>{
console.log(res);
return new Promise((resolve,reject)=>{
setTimeout(()=>{
let data="商品数据";
resolve(data);
},3000)
})
}).then((res)=>{
console.log(res);
});
当有大量的then操作时,其实也不利于代码的阅读与维护。
Generator
async/await能够解决Promise的链式调用带来的代码阅读困难的问题,async/await的原理是在Generator基础之上。在理解Generator之前,先弄清楚一个概念:协程
协程
协程可以理解为“协作的线程”。一个线程可以包含多个协程,但一个线程只能运行一个协程。它的运行流程大概是:A协程开始执行,协程A执行到一半,进入暂停,执行权交给协程B,过了一段时间,协程B交换执行权;最后,协程A恢复执行。
Generator简介
在一般的函数中,我们调用某个函数,这个函数会一直执行完,函数中途不能暂停。Generator也是一个函数,它在function与函数名间插入一个*号,这个函数可以暂停,暂停的标志为yield。yield就是把协程的执行权交出外部去,主线程获得协程的执行权之后执行主线程中的任务。如果想恢复函数的执行权则需要执行生成器的next()函数。
Generator函数的示例代码:
function * bar()
{
console.log('step 1');
yield 1;
console.log('step 2');
yield 2;
console.log('step 3');
yield 3;
}
const gen=bar();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
const gen=bar()并不能执行生成器的函数,只是获得生成器的一个内部指针,它返回的是一个迭代器。如果想执行bar()函数,则需要通过next()函数获得执行权,gen.next().value返回的是一个对象:{value:yield表达式后返回的值;done:true(还可以继续迭代)},获得执行权后如果遇到yield也会将函数的执行权交出去,这就是协程原理。如果还想继续执行bar()函数,则需要通过next()函数拿回执行权。
通过Generator来改写上述获取数据的代码:
function getUsers()
{
setTimeout(()=>{
let data="获取用户数据";
iterator.next(data);
},3000);
}
function getOrders()
{
setTimeout(()=>{
let data="获取订单数据";
iterator.next(data);
},3000);
}
function getGoods()
{
setTimeout(()=>{
let data="获取商品数据";
iterator.next(data);
},3000);
}
function * gen()
{
let users=yield getUsers();
console.log(users);
let orders=yield getOrders();
console.log(orders);
let goods=yield getGoods();
console.log(goods);
}
let iterator=gen();
iterator.next();
以上的代码更像同步执行的代码。
async/await
Generator生成器需要我们手动调用next()函数来拿回执行权继续执行函数里的内容,aysnc/await则内置执行器,不需要我们去调用。首先,先介绍一下async/await的语法。
async用来修饰一个函数,这个函数返回一个Promise对象,await修饰一个表达式,这个表达式一般返回一个Promise对象,await返回的是Promise成功的值。
await做了两件事,第一件事就是创建一个Promise,第二件事就是将主线程的执行权返回给父协程,相当于yield,并且返回创建的Promise的对象。
当Promise对象的状态发生改变的时候,父协程将执行权交还给函数(这就是内置的夺回执行权),继续执行async函数中await语句后的代码。使用async/await的代码改写如下:
function getUsers(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("获取用户的数据");
})
},3000)
}
function getOrders()
{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("获取订单的数据");
},3000)
})
}
function getGoods()
{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve("获取商品的数据");
},3000)
})
}
async function getMessages()
{
let user=await getUsers();
console.log(user);
let orders=await getOrders();
console.log(orders);
let goods=await getGoods();
console.log(goods);
}
getMessages();
异步编程的发展史:回调函数->Promise->Generator->async/await