js中异步编程的理解(回调函数,Promise,async/await)
问题
在js中使用异步编程向后台申请数据的时候,我们如何处理相应的操作呢?
问题:你需要在页面上显示购物车的总价格,现在向后台申请商品的数量,获取到商品的数量之后,你才能计算出总价格,那么显然你需要使用异步的形式获取后台的数据,但是计算价格的代码该怎么执行呢?
回调函数解决方式
这就是一个很经典的问题:当一个方法需要另一个方法的返回结果的才能执行的时候该如何处理,在ES5中,回调函数完全可以解决
回调函数的理解:某个方法a自己没调用,但是在另一个方法b被调用的时候,顺便把方法a给调用了,那么方法a就是一个回调函数
回调函数解决问题:
// 这里我用setTimeout模拟异步请求,使用random函数随机获取商品的数量(假设每件商品的单价为50),最后需要输出总价
let getTotalPrice = (callback) => {
console.log("获取商品数量中。。。")
setTimeout(() => {
let count = parseInt(Math.random() * 10)
// 这里获取到数据了之后执行一个回调函数
callback(count)
}, 2000);
}
let calculate = (count) => {
//定义单价
let price = 50;
console.log(`商品数量为:${count},商品总价为:${price * count}元`);
}
getTotalPrice(calculate);
结果:
但是这样会引发新的问题,那就是当回调的次数过多的时候,需要一环套一环的编写回调函数,会引起js中一个很经典的说法:回调地狱。
好在ES6中推出了一个处理异步的对象:Promise(承诺)
Promise解决方式
Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
Promise 对象代表一个异步操作,有三种状态:
- pending: 初始状态,不是成功或失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
Promise是一个对象,它的参数是一个回调函数,这个回调函数里面有两个参数:resolve和reject,分别代表成功状态fulfilled执行的代码和失败状态rejected执行的代码
需要注意的是,Promise里面的代码本来是当作同步代码执行的,但是一旦遇到异步代码,就会挂起为pending,然后才有后面的异步操作
Promise解决问题:
// 这里我用setTimeout模拟异步请求,使用random函数随机获取商品的数量(假设每件商品的单价为50),最后需要输出总价
new Promise((resolve,reject) => {
console.log("开始请求数据。。。");
setTimeout(() => {
// 这里,我们考虑服务器传来错误的数据,比如负的数量,然后进入reject里面
let count = parseInt(Math.random() * 10) - 5; // [-5,5)
if (count >= 0){
resolve(count);
}else{
reject(count);
}
}, 1000);
}).then(data => {
let price = 50;
console.log(`商品的数量为:${data},总价为:${data * price}`)
}).catch(err => {
console.log(`错误的商品数量:${err}`)
})
结果:
这样就可以很好的模拟向后台请求数据的过程了,如果请求完数据之后,还有判断总价是否超过99,如果超过了就打9折,那么只需要多写一个then即可,比回调函数清晰的多
注意:需要注意的是,then只能写在promise对象上,所以如果想写多个then,则需要在每个then里面重新编写一个promise对象,然后return出去,就可以继续往下写then了
多个promise
// 多个promise的编写
new Promise((reslove, reject) => {
console.log(`开始向后台获取商品数量。。。`)
setTimeout(() => {
let count = parseInt(Math.random() * 10) - 5;
if (count >= 0) {
reslove(count);
} else {
reject(count);
}
}, 2000);
}).then(res => {
console.log(`商品数量正确,为:${res}`)
// 开启一个新的promise,用于计算商品的总价是否大于99(假设商品单价为50)
let totalPrice = res * 50;
let p = new Promise((reslove, reject) => {
if (totalPrice > 99) {
reslove(totalPrice);
} else {
reject(totalPrice);
}
})
// 如果想继续往下编写then的话,这里必须要返回一个promise对象,因为then必须要学在promise对象后面
return p;
}).then(res => {
console.log(`商品的总价超过了99,打9折,打折后的价格为:${res * 0.9}`);
}).catch(err => {
if (err >= 0) {
// 如果参数是大于等于0的,说明不是第一个promise的reject
console.log(`商品的总价没超过99,不打折,价格为:${err}`)
} else {
console.log(`商品的数量不能为负数,${err}`)
}
})
结果:
但是,如果then多了还是不够优雅,看着不习惯,因为一般我们的阅读代码的习惯是一行一行的从向往下看,如果可以将异步代码写成这种形式就好了,ES7就实现了这个功能,使用async/await
async/await解决方式
async其实可以理解为promise的语法糖,它将promise的编写形式改写为人们更容易理解的串行代码的形式,使得异步代码像同步代码
async 是一个修饰符,被它修饰的函数叫异步函数,会默认的返回一个 Promise 的 resolve的值。
await也是一个修饰符,它后面修饰的函数必须返回一个promise对象,它将异步代码转换为同步结果,会阻塞线程,执行完成后才能执行后面的代码,但是必须用在被async修饰的函数里面
async/await解决问题:
// 使用async/await修改之前的promise
// 获取商品数量的方法
let getCount = () => {
return new Promise((resolve, reject) => {
console.log("开始请求数据。。。");
setTimeout(() => {
// 这里,我们考虑服务器传来错误的数据,比如负的数量,然后进入reject里面
let count = parseInt(Math.random() * 10) - 5; // [-5,5)
if (count >= 0) {
resolve(count);
} else {
reject(count);
}
}, 1000);
})
}
// 计算出总价的方法
let totalPrice = (count) => {
console.log("开始计算商品总价。。。");
let p = new Promise((resolve,reject) => {
setTimeout(() => {
let price = count * 50;
if (price >= 99){
resolve(price);
}else{
reject(price);
}
}, 1000);
})
return p;
}
let getPrice = async () => {
try{
// 使用await执行
let count = await getCount();
console.log(`商品的数量为:${count}`);
let price = await totalPrice(count);
console.log(`商品的总价是:${price}`);
}catch(error){
console.log(`进入reject,error:${error}`)
}
}
getPrice();
结果:
这里除去上面的两部异步请求的方法,可以看到最后面那个async修饰的函数,里面使用await,非常的易于理解,就像是同步代码一样,下一行代码必须等上一次代码执行完成才能继续执行。说的底层一点就是await会阻塞线程,延迟执行await语句后面的语句。