1. 异步编程概述
说到JavaScript中的异步编程,我们首先需要明确什么是异步。异步指的是一个程序的执行在某个操作完成之后才能继续执行下去。相对的,同步指的则是程序的执行必须按照预定的顺序依次执行,某个操作必须完成后才能执行下一个操作。
JavaScript中的异步编程主要是因为JavaScript本身是单线程的语言。在处理一些复杂的任务时,如果采用同步编程的方式,程序就会出现阻塞,导致用户操作卡顿或者页面假死。因此,异步编程成为了JavaScript中一个重要的话题。
2. 回调函数的局限性
2.1 问题
在ES5中,我们经常使用回调函数来处理异步编程的问题。回调函数是一种将函数作为参数传递给其他函数,并将这个函数留到稍后执行的编程模式。例如,下面这段代码是一个利用回调函数处理异步编程的示例:
function fetchData(callback) {
setTimeout(() => {
callback('hello world');
}, 1000);
}
fetchData(data => {
console.log(data);
});
这段代码的含义是:1秒后执行callback回调函数,这个回调函数接受一个参数,即字符串'hello world'。最后我们将fetchData函数的回调函数传入该函数中,等待1秒后执行该回调函数,打印出传入的字符串。
回调函数确实解决了JavaScript中的异步编程问题,但是回调函数也有其局限性:
回调函数嵌套过多,导致代码难以维护
回调函数中的错误不易捕获
例如,下面这段代码演示了回调函数嵌套过多导致代码难以管理的例子:
function fetchData(callback) {
setTimeout(() => {
callback('hello world');
}, 1000);
}
function processData(data, callback) {
setTimeout(() => {
callback(data.toUpperCase());
}, 1000);
}
function displayData(data, callback) {
setTimeout(() => {
callback('*** ' + data + ' ***');
}, 1000);
}
fetchData(data => {
processData(data, newData => {
displayData(newData, transformedData => {
console.log(transformedData);
});
});
});
我们将异步操作分为三个步骤:首先fetchData方法获取数据,processData方法对数据进行处理,displayData方法将处理后的数据进行展示。将三个方法放在一起看,代码结构非常深奥,可读性差,维护性差。
2.2 解决方案
解决回调函数嵌套过多的问题,出现了Promise、Generator、async/await等异步编程解决方案。
3. async/await
3.1 async/await概述
async/await是ES2017引入的异步编程方案,其本质上是基于Promise的封装。async函数是一种声明异步函数的方式,函数内部使用await来处理异步操作,使得代码像使用同步操作一样简洁易读。
async函数的语法格式如下:
async function functionName() {
// do something
}
在函数内部,我们可以使用await关键字来等待Promise对象的resolve方法执行成功。例如:
async function fetchData() {
let res = await fetch('https://api.github.com/users');
let data = await res.json();
return data;
}
上面这段代码,我们定义了一个fetchData函数,其中调用了fetch函数获取数据。由于fetch函数返回的是一个Promise对象,我们可以在其后面使用await关键字等待Promise对象的状态变更,获取Promise对象转化后的数据并返回。
3.2 async/await结合Promise使用
虽然async/await是基于Promise的封装,但是对于一些复杂的异步操作,单用async/await可能无法满足需求。例如,我们需要一个函数在每秒钟输出一条信息,在输出信息的同时,还需要等待Promise对象的状态变更。为了实现这个需求,我们需要结合Promise来使用。
function resolveAfterSeconds(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved ' + time);
}, time * 1000);
});
}
async function asyncCall() {
for (let i = 1; i <= 3; i++) {
let result = await resolveAfterSeconds(i);
console.log(result);
}
}
asyncCall();
上面这段代码,我们定义了一个resolveAfterSeconds函数,该函数接受一个时间参数来模拟异步调用需要消耗的时间。我们使用Promise对象来返回异步调用的结果。而在asyncCall函数内部,我们使用await关键字等待Promise对象状态的变更,并在执行成功后输出结果。由于异步操作是模拟的,输出结果如下:
resolved 1
resolved 2
resolved 3
3.3 try/catch捕获错误
使用async/await的优势在于它可以很方便地捕获异步操作中的错误。我们可以使用try/catch语句来处理await关键字返回的Promise对象可能抛出的错误。
async function fetchData() {
try {
let res = await fetch('https://api.github.com/invalid_url');
let data = await res.json();
return data;
} catch(e) {
console.error(e);
}
}
fetchData();
上面的代码中,我们使用了try/catch语句来捕获Promise对象抛出的错误。打印出错误信息如下:
TypeError: Failed to fetch
4. 结论
总的来说,async/await是JavaScript中异步编程的一种强大的解决方案。在比较复杂的异步编程场景下,它可以很方便地结合Promise来使用。而且,在处理异步编程中可能出现的错误时,也可以很方便地使用try/catch语句来捕获错误。相对于回调函数和Promise,async/await简化了代码,增加了可读性和可维护性。