介绍
在web开发中,我们经常需要处理异步操作------这些操作可以在执行其他任务的同时等待完成。我们会向网络、数据库或类似的操作发送请求。JS是非阻塞的,不会停止运行直到代码执行完,而是使用更高效的事件循环,在等待异步任务完成时转而去执行其他任务
事件循环(Event loop):js处理异步操作的机制,负责协调同步任务和异步任务的执行顺序
起初,js使用回调函数来进行一步操作。回调函数的问题就在于,它们导致代码嵌套过于复杂,使代码难以阅读、调试、和拓展。随着es6的发布,js引入了原生promise,使我们可以编写更易读的代码,js不算发展,在es8中引入了一种新的语法来处理异步操作:async...await
async...await语法使我们能够编写的异步代码看起来更像传统的同步命令式程序
async...await 语法实际上是一种语法糖(syntactic sugar) ------它并未引入新的功能,而是为 Promise 和生成器(generators)提供了一种新的使用方式,而这些功能本身早已内置于 JavaScript 语言中。尽管如此,async...await 显著提升了代码的可读性 和可扩展性。
使用回调函数实现异步:
js
fs.readFile('./file.txt', 'utf-8', (err, data) => {
if (err) throw err;
let firstSentence = data;
fs.readFile('./file2.txt', 'utf-8', (err, data) => {
if (err) throw err;
let secondSentence = data;
console.log(firstSentence, secondSentence);
});
});
用promise实现异步:
js
let firstSentence;
promisifiedReadfile('./file.txt', 'utf-8')
.then((data) => {
firstSentence = data;
return promisifiedReadfile('./file2.txt', 'utf-8');
})
.then((data) => {
let secondSentence = data;
console.log(firstSentence, secondSentence)
})
.catch((err) => {console.log(err)});
用async......await实现异步:
js
async function readFiles() {
let firstSentence = await promisifiedReadfile('./file.txt', 'utf-8');
let secondSentence = await promisifiedReadfile('./file2.txt', 'utf-8');
console.log(firstSentence, secondSentence);
}
async关键字
async用于处理异步操作的函数,我们将异步逻辑封装在一个函数中,并在前面加上async关键字,之后调用函数:
js
async function myFunc() {
// Function body here
};
myFunc();
我们通过使用async函数来声明,但是我们也可以用async来创建函数表达式
js
const myFunc = async () => {
// Function body here
};
myFunc();
async函数通常会返回一个promise,这意味着我们可以在async上使用传统的promise语法类似then,catch。一个async函数会返回三种状态:
- 如果没有任何返回值,则该函数会返回一个promise和一个undefined解析值
- 如果async返回的是一个非promise的值,它将返回一个解析为该值的promise
async 总是返回一个promise,无论函数内部返回的是一个普通值还是一个promise
- 当返回非promise时:
js
async function example() {
return 42;
}
example().then(console.log); // 输出:42
- example作为async函数,返回的是一个promise,即使return的值(42)本身就是一个普通值
- JavaScript 自动将 42 包装成
Promise.resolve(42)
,所以我们可以用.then()
处理它。
- 返回promise值
js
async function example2() {
return Promise.resolve("Hello");
}
example2().then(console.log); // 输出:Hello
example2()
内部返回了一个Promise
,不会再额外包装,而是直接返回这个Promise
。
例子:
js
function withConstructor(num){
return new Promise((resolve, reject) => {
if (num === 0){
resolve('zero');
} else {
resolve('not zero');
}
});
}
withConstructor(0)
.then((resolveValue) => {
console.log(` withConstructor(0) returned a promise which resolved to: ${resolveValue}.`);
});
// Write your code below:
const withAsync = async(num) => {
if(num === 0){
return 'zero';
}else{
return 'not zero';
}
}
withAsync(0)
.then((resolveValue) => {
console.log(` withConstructor(0) returned a promise which resolved to: ${resolveValue}.`);
});
第一个函数通过new promise的方式返回promise,第二个函数用async自动包装promise的方式返回promise
await操作符
在上一个练习中,我们理解了async关键字,但这个关键字很少单独使用,大多数时候和函数体内的await关键字一起使用
await关键字只可以用在async函数内部,await是一个操作符:它会返回promise解析的值。由于promise解析的时间是不确定的,await会暂停(或阻塞)async函数的执行,直到指定的promise被解析
在大多数情况下,我们处理的是从函数返回的promise。通常,这些函数来自某个库。
在async函数内部,我们可以使用await来等待它返回的promise解析完成。在下面的例子中,myPromise()是一个会返回promise并解析成字符串"I am resolved now!"的函数
js
async function asyncFuncExample(){
let resolvedValue = await myPromise();
console.log(resolvedValue);
}
asyncFuncExample(); // Prints: I am resolved now!
例如:
js
const brainstormDinner = require('./library.js');
// Native promise version:
function nativePromiseDinner() {
brainstormDinner().then((meal) => {
console.log(`I'm going to make ${meal} for dinner.`);
});
}
// async/await version:
async function announceDinner() {
// Write your code below:
let meal = await brainstormDinner();
console.log(`I'm going to make ${meal} for dinner.`);
}
announceDinner();
编写异步函数
我们已经知道await关键字会暂停执行async函数直到promise不再是pending。所以不要忘记await关键字,如果没有加上的话,我们的函数仍然会运行,只是不会得到预期的结果
例如:
js
let myPromise = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Yay, I resolved!')
}, 1000);
});
}
现在我们写两个函数去调用promise
js
async function noAwait() {
let value = myPromise();
console.log(value);
}
async function yesAwait() {
let value = await myPromise();
console.log(value);
}
noAwait(); // Prints: Promise { <pending> }
yesAwait(); // Prints: Yay, I resolved!
第一个异步函数没有await,第二个异步函数带了await
没有await的异步函数中,函数执行没有暂停,所以会打印出pending,证明console.log在promise变为settle前就已经执行
记住await操作返回promise的解析值 ,当在yesAwait函数中正确使用await时,变量value被赋值为变量 value
被赋值为 myPromise()
解析后的值,而在 noAwait()
中,value
被赋值为 Promise
对象本身。
promise的解析值:当promise进入fulfilled状态后返回的具体值,即是
resolve(value)
传递的value
。 如果Promise
进入rejected
状态(即reject(error)
被调用),那么它没有解析值,而是有一个 拒绝的原因(rejection reason) 。.catch()
处理rejected
状态,并输出错误信息。
处理独立的promise
async await真正的魅力在于当我们一系列相互依赖的异步操作时,它能使代码更清晰易读。例如,我们可能要根据数据库查询的结果发起网络请求,在这种情况下,我们必须等到数据库查询完成后,才能进行网络请求
如果是原生promise,我们需要通过then函数链式调用来实现,then会确保返回正确的:
js
function nativePromiseVersion() {
returnsFirstPromise()
.then((firstValue) => {
console.log(firstValue);
return returnsSecondPromise(firstValue);
})
.then((secondValue) => {
console.log(secondValue);
});
}
但如果我们用async去实现:
js
async function asyncAwaitVersion() {
let firstValue = await returnsFirstPromise();
console.log(firstValue);
let secondValue = await returnsSecondPromise(firstValue);
console.log(secondValue);
}
还有类似的例子,可以使异步操作按序依次运行:
js
const {shopForBeans, soakTheBeans, cookTheBeans} = require('./library.js');
// Write your code below:
const makeBeans = async() => {
let type = await shopForBeans();
let isSoft = await soakTheBeans(type);
let dinner = await cookTheBeans(isSoft);
console.log(dinner);
}
makeBeans();
处理错误
当catch用于较长的promise链时,并不会指示错误发生在链中的具体位置,这会使调试变得困难
和async await类似,我们使用try catch来处理错误。通过使用这个语法,我们不仅可以像处理同步代码一样处理错误,还可以捕获同步和异步错误,这使得调试变得更容易
js
async function usingTryCatch() {
try {
let resolveValue = await asyncFunction('thing that will fail');
let secondValue = await secondAsyncFunction(resolveValue);
} catch (err) {
// Catches any errors in the try block
console.log(err);
}
}
usingTryCatch();
记住,由于async函数返回的是promise,我们仍然可以使用原生promise的catch方法来处理async函数错误
js
async function usingPromiseCatch() {
let resolveValue = await asyncFunction('thing that will fail');
}
let rejectedPromise = usingPromiseCatch();
rejectedPromise.catch((rejectValue) => {
console.log(rejectValue);
})
这有时会在全局作用域中使用,以捕获复杂代码中的最终错误。
js
const cookBeanSouffle = require('./library.js');
// Write your code below:
const hostDinnerParty = async() => {
try{
let beanSouffle = await cookBeanSouffle();
console.log(`${beanSouffle} is served!`)
}catch(error){
console.log(error);
console.log('Ordering a pizza!');
}
}
hostDinnerParty();
处理依赖promise
我们都记得await会停止异步函数的执行,这允许我们方便地书写处理独立promise的异步代码,但是当我们的异步中包含多个promise,且是相互依赖的,应该如何去做?
js
async function waiting() {
const firstValue = await firstAsyncThing();
const secondValue = await secondAsyncThing();
console.log(firstValue, secondValue);
}
async function concurrent() {
const firstPromise = firstAsyncThing();
const secondPromise = secondAsyncThing();
console.log(await firstPromise, await secondPromise);
}
在waiting函数中,我们暂停了函数直到第一个promise成为resolved,然后我们构建第二个函数,一旦resolved,我们就把两个解析的数值打印到控制台。
在concurrent()函数中,两个promise都没有使用await,然后我们等待它们的resolved,并将结果打印到控制台
在我们的concurrent()函数中,两个promise的异步操作可以同时运行,如果可能的话,我们希望尽早开始每个异步操作。在我们的异步函数中,我们仍然应该利用并发性,即同时执行异步函数的能力
注意:如果我们有多个真正相互独立的promise并希望它们能够并行执行,我们必须使用单独的then函数,并且避免使用await造成阻塞
js
let {cookBeans, steamBroccoli, cookRice, bakeChicken} = require('./library.js');
// Write your code below:
const serveDinner = async() => {
const vegetablePromise = steamBroccoli();
const starchPromise = cookRice();
const proteinPromise = bakeChicken();
const sidePromise = cookBeans();
console.log(`Dinner is served. We're having ${await vegetablePromise}, ${await starchPromise}, ${await proteinPromise}, and ${await sidePromise}.`)
}
serveDinner();
可以在字符串构造中使用await运算符
await promise.all()
当我们有多个promise时,另一个提高并发性的方法是使用await和promise.all
promise.all可以将promise数组作为参数,并且会返回单个promise。如果所有的promise都为resolve,则返回的promise也为resolve,且解析值为由每个promise的解析值为元素所构成的数组
js
async function asyncPromAll() {
const resultArray = await Promise.all([asyncTask1(), asyncTask2(), asyncTask3(), asyncTask4()]);
for (let i = 0; i<resultArray.length; i++){
console.log(resultArray[i]);
}
}
在上面的例子中,我们await了promis.all的结果。
promise.all允许我们利用异步特性------四个异步任务可以并发执行
promise.all还有一个快速失败 的优点,这意味着一旦其中任何一个promise被拒绝,它不会等待其他异步操作完成,而是立即返回一个同样的rejected。就像使用原生 Promise 时一样,如果多个异步任务都是必需的,并且它们之间不需要相互等待才能执行,那么 Promise.all()
是一个不错的选择。
js
let {cookBeans, steamBroccoli, cookRice, bakeChicken} = require('./library.js');
// Write your code below:
const serveDinnerAgain = async() => {
let foodArray = await Promise.all([steamBroccoli(), cookRice(),bakeChicken(), cookBeans()]);
let vegetable = foodArray[0];
let starch = foodArray[1];
let protein = foodArray[2];
let side = foodArray[3];
console.log(`Dinner is served. We're having ${vegetable}, ${starch}, ${protein}, and ${side}.`);
}