异步 JavaScript 简单指南:回调、Promises 和 async/await
JavaScript 中的异步编程是编写更好的 JavaScript 所需要掌握的基本概念之一。
今天,我们将学习异步 JavaScript,并结合一些实际示例和一些实际案例。通过本文,您将了解以下功能:
- 异步回调
- 承诺
- 异步/等待
目录
1 - 同步 vs 异步
在讨论异步编程之前,我们先来讨论一下同步编程。
例如,
let greetings = "Hello World.";
let sum = 1 + 10;
console.log(greetings);
console.log("Greetings came first.")
console.log(sum);
您将按照此顺序获得输出。
Hello World.
Greetings came first.
11
这是同步的。请注意,当每个操作发生时,其他任何事情都不会发生。
JavaScript 的核心是单线程:当执行一个代码块时,不会执行其他代码块。
异步编程则不同。简单来说,当 JavaScript 识别出异步任务时,它会继续执行代码,同时等待这些异步任务完成。
异步编程通常与并行化相关,并行化是并行执行独立任务的艺术。
这怎么可能呢?
相信我,我们在不知不觉中就以异步的方式做事。
让我们举一个现实生活中的例子来更好地理解。
现实生活中的例子:咖啡店
杰克来到咖啡店,径直走向第一位服务员。(主线)
- 杰克:你好。可以给我一杯咖啡吗?(第一个异步任务)
- 首席服务员:当然可以。您还需要点别的吗?
- 杰克:等咖啡煮好,真是小菜一碟。(第二个异步任务)
- 首席服务员:当然可以。(开始准备咖啡)
- 首席乘务员:还有什么吗?
- 杰克:不。
- 首席乘务员:请给我 5 美元。
- 杰克:付了钱就坐。
- 第一位服务员:开始为下一位顾客服务。
- 杰克:等待的时候开始查看 Twitter。
- 第二位服务员:这是您的蛋糕。(第二个异步任务调用返回)
- 杰克:谢谢
- 第一位服务员:这是您的咖啡。(第一个异步任务调用返回)
- 杰克:嘿,谢谢!把他的东西拿走吧。
简单来说,当您等待别人为您做事时,您可以做其他任务或要求别人为您做更多的事情。
现在您已经清楚地了解了异步编程的工作原理,让我们看看如何使用以下内容编写异步代码:
- 异步回调
- 承诺
- 和
async/await
语法。
2 – 异步回调:一旦我完成,我会回调!
回调函数是在调用函数(高阶函数)时作为参数传递的函数,该函数将在后台开始执行任务。
当此后台任务完成运行时,它会调用回调函数来通知您更改。
function callBackTech(callback, tech) {
console.log("Calling callBackTech!");
if (callback) {
callback(tech);
}
console.log("Calling callBackTech finished!");
}
function logTechDetails(tech) {
if (tech) {
console.log("The technology used is: " + tech);
}
}
callBackTech(logTechDetails, "HTML5");
输出
正如您在这里看到的,代码每行之后都会执行一次:这是同步执行回调函数的示例。
如果你经常使用 JavaScript 编写代码,你可能已经在不知不觉中使用过回调函数了。例如:
array.map(callback)
array.forEach(callback)
array.filter(callback)
let fruits = ['orange', 'lemon', 'banana']
fruits.forEach(function logFruit(fruit){
console.log(fruit);
});
输出
orange
lemon
banana
但回调也可以异步执行,这仅仅意味着回调比高阶函数执行得晚。
让我们重写我们的例子,使用setTimeout()
函数来注册一个异步调用的回调。
function callBackTech(callback, tech) {
console.log("Calling callBackTech!");
if (callback) {
setTimeout(() => callback(tech), 2000)
}
console.log("Calling callBackTech finished!");
}
function logTechDetails(tech) {
if (tech) {
console.log("The technology used is: " + tech);
}
}
callBackTech(logTechDetails, "HTML5");
输出
在这个异步版本中,请注意的输出logTechDetails()
打印在最后的位置。
这是因为此回调的异步执行将其执行延迟了 2 秒,直到当前正在执行的任务完成为止。
回调是old-fashioned
编写异步 JavaScript 的方式,因为一旦您必须处理多个异步操作,回调就会相互嵌套,以 结尾callback hell
。
为了避免这种模式的发生,我们现在就来看看Promises
。
3-承诺:我保证一定的结果!
Promises用于处理 JavaScript 中的异步操作,它们只是表示异步操作的完成或失败。
因此,Promises 有四种状态:
- pending:承诺的初始状态
- fulfilled:操作成功
- 拒绝:操作失败
- 已结算:操作已完成或已结算,但不再待处理。
这是在 JavaScript 中创建 Promise 的一般语法。
let promise = new Promise(function(resolve, reject) {
... code
});
resolve
和reject
分别是操作成功和操作失败时执行的函数。
为了更好地理解Promises
工作原理,让我们举一个例子。
- 杰克的妈妈:嘿,杰克!你能去商店买点牛奶吗?我需要一些牛奶来完成蛋糕。
- 杰克:当然可以,妈妈!
- 杰克的妈妈:你做这些的时候,我会准备好做蛋糕的工具。(异步任务)同时,如果你找到了,请告诉我。(成功回调)
- 杰克:太好了!可是如果我找不到牛奶怎么办?
- 杰克的妈妈:那你就吃点巧克力吧。(失败回调)
这个比喻并不是十分准确,但我们还是采用它吧。
假设杰克找到了一些牛奶,承诺将会是这样的。
let milkPromise = new Promise(function (resolve, reject) {
let milkIsFound = true;
if (milkIsFound) {
resolve("Milk is found");
} else {
reject("Milk is not found");
}
});
那么,这个承诺可以像这样使用:
milkPromise.then(result => {
console.log(result);
}).catch(error => {
console.log(error);
}).finally(() => {
console.log("Promised settled.");
});
这里 :
then()
:在成功的情况下进行回调,并在承诺得到解决时执行。catch()
:接受回调,如果失败则执行承诺,如果承诺被拒绝则执行。finally()
:接受回调函数,并在前提条件确定后返回。当你需要执行一些清理操作时,它非常有用。
现在让我们使用一个真实的例子,通过创建一个承诺来获取一些数据。
let retrieveData = url => {
return new Promise( function(resolve, reject) {
let request = new XMLHttpRequest();
request.open('GET', url);
request.onload = function() {
if (request.status === 200) {
resolve(request.response);
} else {
reject("An error occured!");
}
};
request.send();
})
};
该XMLHttpRequest
对象可用于在 JavaScript 中发出 HTTP 请求。
让我们使用https://swapi.dev发出星球大战 APIretrieveData
的请求。
const apiURL = "https://swapi.dev/api/people/1";
retrieveData(apiURL)
.then( res => console.log(res))
.catch( err => console.log(err))
.finally(() => console.log("Done."))
输出结果如下。
输出
撰写承诺的规则
- 您不能在代码中同时调用
resolve
或reject
。一旦两个函数之一被调用,Promise 就会停止并返回结果。 - 如果您不调用这两个函数中的任何一个,承诺就会挂起。
- 您只能向
resolve
或传递一个参数reject
。如果您要传递更多内容,请将所有内容包装在一个对象中。
4 - async/await:当我准备好时就会执行!
该async/await
语法是随ES2017引入的,旨在帮助使用承诺编写更好的异步代码。
那么,promise 有什么问题呢?
你可以随意串联then()
多个 promise,但这样写起来会Promises
有点冗长。
以杰克买牛奶为例,他可以:
- 打电话给他的妈妈;
- 然后买更多的牛奶;
- 然后买巧克力;
- 等等。
milkPromise.then(result => {
console.log(result);
}).then(result => {
console.log("Calling his Mom")
}).then(result => {
console.log("Buying some chocolate")
}).then(() => {
...
})
.catch(error => {
console.log(error);
}).finally(() => {
console.log("Promised settled.");
});
让我们看看如何使用async/await
JavaScript 编写更好的异步代码。
朋友聚会示例
杰克被朋友邀请参加一个聚会。
- 朋友:你什么时候准备好?我们会来接你。
- 杰克:20分钟就好。我保证。
嗯,其实杰克30分钟就能准备好。顺便说一句,他的朋友们没等他就去不了派对,所以他们得等着。
以同步的方式,事情看起来会像这样。
let ready = () => {
return new Promise(resolve => {
setTimeout(() => resolve("I am ready."), 3000);
})
}
该setTimeout()
方法将函数作为参数(回调)并在指定的毫秒数后调用它。
让我们Promise
在常规函数中使用它并查看输出。
function pickJack() {
const jackStatus = ready();
console.log(`Jack has been picked: ${jackStatus}`);
return jackStatus;
}
pickJack(); // => Jack has been picked: [object Promise]
为什么会出现这样的结果?Promise
因为函数没有很好地处理pickJack
。
它被当作jackStatus
一个普通的对象。
现在是时候告诉我们的函数如何使用async
和await
关键字来处理这个问题。
首先,将async
关键字放在函数前面pickJack()
。
async function pickJack() {
...
}
通过async
在函数前使用 关键字,JavaScript 会理解该函数将返回一个Promise
。
即使我们没有明确返回Promise
,JavaScript 也会自动将返回的对象包装在 Promise 中。
下一步,await
在函数主体中添加关键字。
...
const jackStatus = await ready();
...
await
使 JavaScript 等待直到Promise
解决并返回结果。
该函数最终看起来是这样的。
async function pickJack() {
const jackStatus = await ready();
console.log(`Jack has been picked: ${jackStatus}`);
return jackStatus;
}
pickJack(); // => "Jack has been picked: I am ready."
这就是全部内容了async/await
。
此语法具有简单的规则:
-
如果您创建的函数处理异步任务,请使用
async
关键字标记该函数。 -
await
关键字暂停函数执行,直到承诺得到解决(履行或拒绝)。 -
异步函数总是返回一个
Promise
。
async/await
下面是使用和方法的实际示例fetch()
。fetch()
允许您发出类似的网络请求,XMLHttpRequest
但这里最大的区别是 Fetch API 使用 Promises。
这将帮助我们使从https://swapi.dev获取的数据更加清晰和简单。
async function retrieveData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Error while fetching resources.');
}
const data = await response.json()
return data;
};
const response = await fetch(url);
将暂停函数执行,直到请求完成。
那么为什么呢await response.json()
?你可能会问自己。
初始fetch()
调用后,仅读取了标头。主体数据需要先从传入流中读取,然后再解析为 JSON。
由于从 TCP 流读取(发出请求)是异步的,因此.json()
操作最终也是异步的。
然后我们在浏览器中执行代码。
retrieveData(apiURL)
.then( res => console.log(res))
.catch( err => console.log(err))
.finally(() => console.log("Done."));
这就是全部async/await
结论
在本文中,我们学习了回调,async/await
以及如何Promise
在 JavaScript 中编写异步代码。如果你想了解更多关于这些概念的知识,请查看这些精彩的资源。
- JavaScript 中 async/await 的有趣解释
- 关于 JavaScript 回调函数的一切
- Promises 基础知识每篇文章都可以变得更好,因此欢迎在评论部分提出您的建议或问题。😉