异步 JavaScript 速成课程(第一部分)
介绍
JavaScript 调用栈
异步是什么意思?
AJAX
什么是API?
回调函数
介绍
异步 JavaScript 的概念可能比较复杂,但它是 JavaScript 程序员工具箱中必不可少的工具。它可以用来提升用户体验。本文后续内容将更深入地探讨异步代码的重要性。
我们将探讨的主题:
第一部分
- JavaScript 调用栈
- 异步是什么意思?
- AJAX
- API 和 Web API
- 回调函数
第二部分
- 承诺
- 异步和等待
- 发出 HTTP 请求
- API项目
先决条件
需要具备JavaScript基础知识。
笔记
我们的目标是了解所有拼图碎片如何拼凑成一幅完整的图景。
让我们先来了解一下JavaScript的工作原理。
JavaScript 调用栈
JavaScript 是一种单线程语言。这意味着 JavaScript 一次只能运行一段代码。它通过一种叫做调用栈的东西来实现这一点。
调用栈是 JavaScript 引擎用来跟踪自身在脚本中的位置并管理不同函数调用的方式。栈是一种遵循后进先出 (LIFO) 原则的数据结构。 在这里我们可以看到“3”位于栈的末尾,因此它将是第一个被执行的,执行完毕后就会从栈中弹出。我们可以使用名为Loupe 的工具来可视化这个过程。
以下是一个例子:
console.log("start");
console.log("End");
- 运行这段代码时,第一个日志会进入调用堆栈。
- 执行完毕后,它会从调用栈中弹出。
- 然后将第二个日志压入堆栈。
- 执行完毕后,程序弹出并结束。
这就是我们所说的同步流程(一个接一个地执行)。
每当我们在谷歌上搜索内容时,都会看到页面刷新并等待响应。这是默认的同步行为。程序会等待响应。
异步是什么意思?
当我们在 YouTube 搜索栏中搜索内容时,只要开始输入,就能立即看到搜索建议。
这实际上意味着,在你输入的每个字母之后,后台都会向服务器发出请求来获取建议。
然而,这并不会影响网站的用户界面,从而提升用户体验。这被称为异步请求。
让我们以 setTimeout 函数为例:
console.log("start")
setTimeout(function(){
console.log("middle")
},1000)
console.log("end")
你认为这段代码的输出结果是什么?
start
middle
end
既然我们看到 JavaScript 是单线程的,那么上面的输出应该是正确的,对吧?但实际输出却与我们预期的不同。
这是脚本的实际输出结果。
start
end
middle
JavaScript 似乎是先打印出开始时间和结束时间,然后异步执行 setTimeout,等待 1 秒后返回结果。那么,为什么这种方法有效呢?JavaScript 是如何做到在持有变量的同时继续执行代码的呢?
所以关键在于,setTimeout 实际上并不是 JavaScript 的一部分,而是我们所说的浏览器 API。我们将在下一节详细了解 API,但请先听我解释一会儿。
Web 浏览器 API 就像浏览器赋予我们的超能力。例如,它可以使用 setTimeout 将某个值保存一段时间,然后再返回该值。
setTimeout 函数的工作原理
- 第一行代码被压入堆栈,并在控制台中打印“start”。
- setTimeout 被压入堆栈。
- 调用堆栈弹出函数,然后向浏览器 API 发出请求:“嘿,浏览器!将函数中的值保留 1 秒钟,并在时间结束后提醒我运行它。”
- 调用栈向前移动,执行下一行代码,并在控制台打印“end”。
-
最后,控制台打印出“middle”。
然而,异步并不局限于 setTimeout 函数。
为什么我们需要异步代码
- 虽然有些请求可能不会花费太多时间,但有些请求,例如从数据库或 API 获取数据,可能需要几秒钟。
- 如果我们同步发出这些请求,由于 JavaScript 一次只能执行一个任务,因此在获取数据期间会阻塞用户界面,从而降低用户体验。
有些事情可能需要异步请求,例如:
- 从数据库获取数据。
- 登录和注册时验证用户身份。
- 从外部 Web API 获取 JSON 数据。
AJAX
- AJAX 代表异步 JavaScript 和 XML。
- AJAX 不是一种技术或工具,而是一种概念。
- 这只是用来描述异步代码的一种术语。
- 当我们与服务器异步交换数据时,这被称为 AJAX 请求。
- 我们还可以在不重新加载网页的情况下更新页面。
那么,ajax 中的 xml 部分是什么意思呢?
- 当我们谈到与运行不同技术的不同服务器交换数据时,必须有一种所有服务器都能理解的单一数据格式。
- XML 和 JSON 为我们提供了这些接口,使我们能够以所有人都能理解的格式传输数据。
XML
- XML是可扩展标记语言(eXtensible Markup Language)的缩写。
- 它与 html 类似,因为它也像 HTML 一样使用标签。
- 然而,关键区别在于,HTML 用于显示数据,而 XML 用于存储和传输数据。
句法
<note>
<date>2015-09-01</date>
<hour>08:30</hour>
<to>Tove</to>
<from>Jani</from>
<body>This is a reminder</body>
</note>
JSON
JSON 是 JavaScript 对象表示法的缩写,也是一种数据传输格式,是 XML 的替代方案。JSON 非常易于阅读和理解。虽然它看起来像 JavaScript 对象,但 JSON 可以独立于 JavaScript 进行数据传输。许多编程语言都具备解析和读取 JSON 的能力。
JSON 由键和值两部分组成。它们共同构成键/值对。
- 密钥:密钥始终是用引号括起来的字符串。
- 值:值可以是字符串、数字、布尔表达式、数组或对象。
JSON 比 XML 更流行。
熟悉 JavaScript 对象的人都能轻松理解 JSON。
现代 API 大多使用 JSON 来传输和存储数据。
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
}
]
}
什么是API?
API是应用程序编程接口(Application Programming Interface)的缩写。API
是一个接口,它包含一组函数,允许程序员访问应用程序、操作系统或其他服务的特定功能或数据。
Web API
在谈到 Web 开发中的 API 时,我们通常所说的 API 指的是“Web API”。
Web API,顾名思义,是一种可以通过HTTP协议访问的基于Web的API。
通常情况下,当我们向网页发出请求时,会获取到各种数据,例如 HTML、CSS 和 JavaScript。相反,当我们通过 JavaScript 发出异步请求时,我们可能只需要其中的特定部分数据。
Web API 接收来自不同类型的客户端设备(如手机、笔记本电脑等)的请求,并将这些请求发送到 Web 服务器进行处理,然后将所需的数据返回给客户端。
例如:Twitter 的 API 提供数据读写权限,我们可以利用这些权限将 Twitter 的功能集成到我们自己的应用程序中。例如,我们可以获取用户推文的数据并在我们自己的应用程序中使用这些数据。
API 提供的数据可以是任何类型,例如:图像、JSON 对象等。
- 稍后我们会详细了解这些 API 的 HTTP 请求,但请先听我解释一会儿。
- API 请求就像你在搜索栏中向谷歌、Facebook 等不同网站发出的任何其他 HTTP 请求一样,但 API 的响应不包含不需要的数据(html、css 等)。
我们举个例子来更好地理解这一点。
这是一个宝可梦API。当我们向它发送请求时,它会返回一张宝可梦的图片。我们需要将id替换为我们想要的宝可梦的id。例如,1是妙蛙种子,以此类推。这是我们向https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/1.png 发送请求后得到的结果。
你们也可以试试!只需将上面的链接粘贴到浏览器中,就能看到图片。你们还可以尝试更改图片 ID,看看会得到什么不同的结果。
这和其他 HTTP 请求一样,只是数据(图片)不同而已。
查询字符串
在 API 的 URL 中,我们需要根据需求替换图片的 ID。
这本质上是一个查询字符串,它会根据我们在查询字符串中传递的值来请求数据。
我们再举一个例子。
这是我们稍后会用到的另一个API。
在URL中,` ?q=:query${queryString}`被称为查询字符串,它:query是一个变量。响应会根据查询变量的值而变化。
我们使用宝可梦 API 做了一个简单的示例,它可以立即返回数据。然而,某些操作,例如从数据库检索信息,可能需要更长时间,甚至可能被拒绝。因此,我们必须考虑如何处理可能出现的错误。
我们将在后面的几节中讨论发起异步请求的各种方法。
但在此之前,我们需要了解如何处理异步请求失败的情况。
回调函数
定义
回调函数是将一个函数作为参数传递给另一个函数,然后在外部函数内部调用该函数来完成某种例程或操作。
什么?!
以下是一个例子:
const success = ()=>(
console.log("success")
)
const fail = ()=>(
console.log("fail")
)
const check = (success,fail)=>{
const a = 2
if(a == 2){
success()
}else{
fail()
}}
check(success,fail)
- 假设有两个函数
success,fail - 我们将这两个函数作为参数传递给第三个函数
check。 - 当
check执行时success,如果变量'a'等于2,则调用该函数;否则,调用该fail函数。 - 由于`
successand`fail函数作为参数传递,并check在某些事件发生后在函数内部调用,因此它们被称为回调函数。
好!让我们再次举办邻里互助setTimeout活动吧。
- 我们知道setTimeout函数有两个参数。第一个参数是一个函数,第二个参数是一个延迟时间。
setTimeout(someFunction,delay)
让我们创建一个函数并将其传递给 setTimeout。
const message = function() {
console.log("I Have been waiting for 3 sec !!!");
}
setTimeout(message,3000)
就像setTimeout一个外部函数调用了作为参数传递的“message”函数一样,这里的messagefunction是一个回调函数。
- 让我们来看看setTimeout是如何定义的。
setTimeout(message,3000){
/*
some code which will hold the value for 3 secs
*/
message()
}
setTimeout我们看到,作为参数传递给的函数(message)是在 setTimeout 中调用的。
传递回调函数可能不是最佳方法
-
假设我们要编写一个脚本,其中需要执行多个异步操作,但前提是前一个操作必须完成。在这种情况下,回调可能不是最佳选择。让我们看看为什么。
-
假设我们要编写一个函数,将主体的背景颜色更改为彩虹的不同颜色。但是,每种颜色应该间隔 1 秒出现。例如,红色在 1 秒后出现,橙色在 2 秒后出现,黄色在 3 秒后出现,依此类推。
- 我们知道可以使用 setTimeout 来延迟颜色显示。但是,我们不能将它们彼此独立地使用,因为我们需要计算延迟时间。
/* assume that red , orange , yellow are
the functions to change the bg color to the respective colour.*/
setTimeout(red,1000);
setTimeout(orange,2000);
setTimeout(yellow,3000);
- 我们可以将一个回调函数传递给一个函数,该函数基本上只在前一个 setTimeout 完成后才运行下一个 setTimeout
setTimeout。 - 它看起来大概是这样的:
const delayedColorChange = (newColor, delay, doNext) => {
setTimeout(() => {
document.body.style.backgroundColor = newColor;
doNext();
}, delay)
}
- 那么,我们该如何调用这个函数呢?
- 假设我们想先把颜色改成红色,然后再改成橙色。
- “doNext”参数将包含一个回调函数,该函数会再次调用delayedColorChange函数,但这次颜色为橙色。类似这样。
delayedColorChanged("red",1000,()={
delayedColorChanged("orange",1000,()=>{
//This function will be empty since we want to end the
//color change
});
})
- 现在假设我们想把橙色改成黄色。
delayedColorChanged("red",1000,()={
delayedColorChanged("orange",1000,()=>{
delayedColorChanged("yellow",1000,()=>{
//This function will be empty since we want to end the
//color change
});
});
})
- 现在让我们来画一道完整的彩虹。
delayedColorChange('red', 1000, () => {
delayedColorChange('orange', 1000, () => {
delayedColorChange('yellow', 1000, () => {
delayedColorChange('green', 1000, () => {
delayedColorChange('blue', 1000, () => {
delayedColorChange('indigo', 1000, () => {
delayedColorChange('violet', 1000, () => {
//This function will be empty since
//we want to end the
//color change
})
})
})
})
})
})
});
- 随着我们不断嵌套更多的回调函数,情况会变得有点复杂。
- 即使这里我们只用了一个回调函数,结果也变得相当复杂。API 或任何类型的异步请求通常都会有两个回调函数:成功回调和失败回调。在这种情况下,就会出现大量的嵌套。
- 假设
fakeRequestCallback(url,success,failure)这是一个虚构的函数,它会请求 URL 获取数据。 success和failure是两个回调函数。- 如果没有错误,
success则调用;否则failure调用。 - 以下是向多个页面发出请求,但仅在前一个请求成功后才发出的请求时发生的情况,类似于彩虹函数。
fakeRequestCallback('books.com/page1',
function (response) {
console.log(response)
fakeRequestCallback('books.com/page2',
function (response) {
console.log(response)
fakeRequestCallback('books.com/page3',
function (response) {
console.log(response)
},
function (err) {
console.log("ERROR (3rd req)!!!", err)
})
},
function (err) {
console.log("ERROR (2nd req)!!!", err)
})
}, function (err) {
console.log("ERROR!!!", err)
})
欢迎来到回调地狱。
如果有多个异步操作要执行,而我们尝试使用传统的回调函数,我们就会陷入回调地狱。
让我们看看第二部分是否有更好的方法。
文章来源:https://dev.to/chinmaymhatre/crash-course-in-asynchronous-javascript-part-1-4g2o








