JavaScript 中的异步编程
你是否在 JavaScript 代码中使用过回调函数、Promise 或最新的 async/await?你是否觉得它们难以理解?你是否曾经好奇过它们底层的工作原理……?那么,让我们一起来掌握它们吧。
引言
对于初学 JavaScript 的人来说,JavaScript 中的异步编程可能会令人困惑,有时甚至经验丰富的程序员也会感到吃力。至少我以前也不了解它的底层原理。我们知道 JavaScript 是单线程的,这意味着它一次只能执行一个任务,这与其他多线程编程语言(例如 Java 和 C#)不同。那么,当我们想从 API 获取数据或在后端执行一些异步数据库操作时该怎么办呢?这时,回调、Promise 或 async/await 就派上用场了。我们不想阻塞 JavaScript 的主线程,但又希望在异步操作完成后收到通知,这就是异步编程概念的用武之地。让我们来看看它们以及它们的演变历程……
异步 JavaScript 的演变
*回调函数
* Promise
* Async/await
回调函数
回调函数就是作为参数传递的函数,你希望在某些操作完成后调用它们。
function add(x,y,callback){
const sum = x+y;
callback(sum);
};
add(2,3,function(sum){
console.log('sum',sum); //sum 5
});
这很简单,我们只需要传入一个要在异步操作完成后执行的函数即可。但是,这种方法的主要问题是,当我们想要进行多个异步调用并且必须一个接一个地执行时……它就会引入所谓的“回调地狱”。类似于下面的代码:
getData(function(a){
getMoreData(a, function(b){
getMoreData(b, function(c){
getMoreData(c, function(d){
getMoreData(d, function(e){
...
});
});
});
});
});
由于每个异步调用都依赖于前一次调用获取的数据,因此它必须等待前一次调用完成。这种方法虽然可行,但调试和维护起来非常困难。让我们来看看 Promise 是如何解决这个问题的。
承诺
Promise 是在 ES6 中引入的,它解决了回调函数的一些问题。每个 Promise 构造函数都接受一个带有两个参数的函数resolve。reject如果resolvePromise 成功解析,则调用该函数;如果 Promise 被拒绝或发生任何错误,则调用该函数。
const promise = new Promise(function(resolve, reject) {
// an API call or any async operation
});
这里,函数参数 `a`resolve和reject`b` 本身也是函数,并且被正确地调用。我们来看一个例子:
const promise = new Promise(function(resolve, reject) {
setTimeout(() => {
resolve("Time is out");
}, 4000);
});
promise
.then(function(data){console.log(data)})
.catch(function(error){console.log('Something bad happened: ',error)})
Promise 就是一个对象,它执行任何异步操作,并根据传递给其回调函数的参数调用 resolve 或 reject 函数。
在上面的setTimeout例子中,我们创建了一个新的 Promise 对象,并将其赋值给一个变量,该变量包含一个带有 resolve 和 reject 参数的回调函数。其内部执行过程如下:
1.第一个Promise尝试执行回调函数内部的代码,即:setTimeout
2. 4 秒后,当setTimeout操作完成后,它会尝试解析,
即调用 resolve 函数。
3.resolve我们作为回调函数参数传递的值将绑定
到Promise类中的另一个函数,我们称之为 `function` onResolved。因此,当在resolve`function` 内部调用 `function`时,它会使用传递给 `function` 的值来调用类中的` setTimeoutfunction` 。这里它是一个字符串。onResolvedPromiseresolveTime is out
4.onResolved函数内部会调用你传递的回调函数,.then()
并将接收到的值传递给它resolve;同样,它也会处理拒绝的情况
。
5. 这是 Promise 内部运作的简化版本,如果您
要链接多个 Promise,情况就会变得稍微
复杂一些……Promise类维护一个回调数组,这些回调会
按照语句的顺序依次调用.then()
。如果您想深入了解,请参阅这篇文章。
因此,使用 Promise 链,你不需要将一个调用嵌套在另一个调用中,你可以将它们一个接一个地链接起来。
假设你想执行两个异步操作,并且想使用一个 Promise 返回的数据来执行另一个异步调用,我们可以使用 Promise 做类似这样的事情:
const promise1 =new Promise(function(resolve,reject){
// async work
})
const promise2 = function(datafromFirst){
return new Promise(function(resolve,reject){
// async work
})
}
promise1
.then(function(data1){console.log(data1); return promise2(data1) })
.then(function(data2){console.log(data2); })
.catch(function(error){console.log(error);//error caught from any of
the promises})
这使得代码更易读、更易理解……但 Promise 链式调用却让代码变得复杂。由于前一个 Promise 必须返回另一个 Promise 才能进行链式调用,调试也变得异常困难……诚然,Promise 让编写异步代码变得更加容易,避免了回调地狱,但我们还能做得更好吗?当然可以!使用 async 和 await 绝对没问题……
异步等待
ES8 的新特性底层async-await使用了相同的promises机制,但它消除了传递回调函数和处理 Promise 链式调用的需要。它提供了更高的抽象级别,代码也变得更加简洁。
async function func(){
try{
const result = await someasynccall();
console.log(result);
}
catch(error){
console.log(error);
}
}
我们需要使用 `await` 关键字async将函数声明为异步函数,只有这样才能await在函数内部使用 `await` 关键字。我们可以将try-catch`await` 代码包裹起来,以便在抛出错误时能够捕获它。
让我们来看前面两个异步调用的例子,我们需要从第一个调用中获取数据,以便使用 async/await 语法进行另一个异步调用。
async function func(){
try{
const data1 = await someasyncall();
const data2 = await anotherasynccall(data1);
console.log(data2);
}
catch(error){
console.log(error);
}
}
这样看起来更简洁,至少写起来更容易……
假设我们想从异步函数返回一些内容,并且之后想使用它,那么我们需要使用立即执行函数表达式(IIFE)模式。
你认为以下代码会console.log(message)记录什么内容?
async function func(){
try{
const result = await someasynccall();
console.log('result',result);
return 'successful';
}
catch(error){
console.log(error);
return 'failed';
}
}
const message = func();
console.log(message)
将会console.log(message)打印出结果Promise{<pending>},但不会打印出实际的“成功”或“失败”,因为我们的console.log函数在内部的 Promiseawait someasynccall()执行完成之前运行。所以,如果我们想要实际使用message值,则需要使用 IIFE(立即调用函数表达式),如下所示:
async function func(){
try{
const result = await someasynccall();
console.log('result',result);
return 'successful';
}
catch(error){
console.log(error);
return 'failed';
}
}
(async function(){
const message = await func();
console.log(message);
})();
因此,我们使用另一个立即调用的异步函数,await让该函数返回消息字符串,然后使用它。
这就是我们处理异步代码的方式,经过多年的发展async-await,现在最新的代码看起来更简洁、更易读。
