理解事件循环和回调

0 股票
0
0
0
0

介绍

在互联网早期,网站通常由HTML页面上的静态数据构成。但如今,随着Web应用程序变得更加交互式和动态,执行诸如外部网络请求以获取API数据等密集型操作变得越来越必要。要在JavaScript中执行这些操作,开发人员必须使用异步编程技术。.

由于 JavaScript 是一种单线程编程语言,采用并发执行模型,按顺序处理操作,因此一次只能处理一个命令。然而,诸如从 API 请求数据之类的操作可能需要不确定的时间,具体取决于请求数据的大小、网络连接速度以及其他因素。如果 API 调用是同步进行的,那么在 API 调用完成之前,浏览器将无法处理任何用户输入,例如滚动或点击按钮。这被称为阻塞。.

为了避免阻塞行为,浏览器环境提供了许多可供 JavaScript 访问的异步 Web API,这意味着它们可以与其他操作并行执行,而不是顺序执行。这非常有用,因为它允许用户在异步操作处理期间继续正常使用浏览器。.

事件循环

本节解释 JavaScript 如何使用事件循环处理异步代码。首先,通过演示事件循环的工作原理,然后解释事件循环的两个组成部分:栈和队列。.

不使用任何异步 Web API 的 JavaScript 代码会同步执行——一次执行一个,按顺序执行。以下示例代码演示了这一点,它调用了三个函数,每个函数都会在控制台打印一个数字:

// Define three example functions
function first() {
console.log(1)
}

function second() {
console.log(2)
}

function third() {
console.log(3)
}

在这段代码中,你定义了三个使用 console.log() 打印数字的函数。.

然后,编写函数调用:

// Execute the functions
first()
second()
third()

输出结果将根据函数的调用顺序而定——先调用 first(),再调用 second(),最后调用 three():

Output
1
2
3

使用异步 Web API 时,规则会变得更加复杂。您可以尝试使用内置的 `setTimeout` API,它用于设置一个计时器,并在指定时间后执行操作。`setTimeout` 必须是异步的,否则在等待期间整个浏览器都会卡死,导致糟糕的用户体验。.

为了模拟异步请求,请在第二个函数中添加 setTimeout:

// Define three example functions, but one of them contains asynchronous code
function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

`setTimeout` 接受两个参数:一个要异步执行的函数,以及调用该函数之前等待的时间。在这段代码中,你将 `console.log` 放在一个匿名函数中,并将其传递给 `setTimeout`,然后将该函数的执行时间设置为 0 毫秒。.

现在像之前一样调用这些函数:

// Execute the functions
first()
second()
third()

你可能会认为,将 setTimeout 设置为 0 后,执行这三个函数仍然会按顺序打印数字。但由于它是异步的,函数会在打印最后一个数字时中断:

Output
1
3
2

无论你将计时器设置为零秒还是五分钟,异步代码调用的 `console.log` 函数都会在顶层同步函数执行完毕后运行。这是因为 JavaScript 宿主环境(在本例中为浏览器)使用了一种称为事件循环的概念来处理并发或并行事件。由于 JavaScript 一次只能执行一个命令,因此它需要事件循环来知道何时执行某个特定的命令。事件循环利用栈和队列的概念来处理这种情况。.

栈,也称为调用栈,保存着当前正在执行的函数的状态。如果您不熟悉栈的概念,可以将其想象成一个具有«后进先出»(LIFO)特性的数组,这意味着您只能从栈底添加或删除元素。JavaScript 会执行栈上的当前帧(或特定环境下的函数调用),然后将其移除并继续执行下一帧。.

例如,如果代码仅包含同步代码,则浏览器按以下顺序执行:

  • 第一的() 将 first() 添加到堆栈中,执行 first(),该函数会向控制台输出 1,然后从堆栈中移除 first()。.
  • 第二() 将 second() 添加到栈中,执行 second(),该函数会在控制台打印 2,然后从栈中移除 second()。.
  • 第三 () 将 third() 添加到堆栈中,执行 third(),该函数会将 3 输出到控制台,然后从堆栈中移除 third()。.

第二个使用 setTimeout 的示例如下:

  • 第一的() 将 first() 添加到堆栈中,执行 first(),该函数会向控制台输出 1,然后从堆栈中移除 first()。.
  • 第二() 将内容添加到堆栈中,执行 second()。.
    • 将 setTimeout() 添加到堆栈中,执行 Web API setTimeout(),该函数会启动计时器并将匿名函数添加到队列中,然后从堆栈中移除 setTimeout()。.
  • 第二() 从堆栈中移除。.
  • 第三 () 将 third() 添加到堆栈中,执行 third(),该函数会将 3 输出到控制台,然后从堆栈中移除 third()。.
  • 事件循环检查队列中是否有待处理的消息,找到 setTimeout() 中的匿名函数,将该函数添加到堆栈中,该函数会向控制台输出 2,然后将其从堆栈中移除。.

使用 setTimeout,异步 Web API 引入了队列的概念,本教程稍后将对此进行介绍。.

队列

队列,也称为消息队列或任务队列,是函数等待的区域。当调用栈为空时,事件循环会检查队列中是否有待处理的消息,从最早的消息开始检查。找到消息后,将其添加到调用栈中,调用栈会执行消息中包含的函数。.

在 `setTimeout` 示例中,匿名函数会在顶层代码执行完毕后立即执行,因为定时器设置为 0 秒。需要注意的是,定时器并非指代码会在 0 秒或任何特定时间执行,而是指它会在定时器设定的时间内将匿名函数添加到队列中。之所以需要这种队列机制,是因为如果定时器在超时后直接将匿名函数添加到堆栈,将会中断当前正在执行的函数,这可能会产生意想不到的后果。.

注意:还有一个名为作业队列或微任务队列的队列,专门用于处理 Promise。像 Promise 这样的微任务比像 setTimeout 这样的宏任务具有更高的执行优先级。.

既然你已经了解了事件循环如何使用栈和队列来处理代码执行顺序,那么下一步就是弄清楚如何控制代码中的执行顺序。为此,你首先要学习确保事件循环正确处理异步代码的主要方法:回调函数。.

回调函数

在 `setTimeout` 示例中,带有超时设置的函数会在顶层执行的主上下文中所有操作完成后执行。但是,如果您希望确保某个函数(例如第三个函数)在超时后执行,则需要使用异步编码技术。这里的超时可以代表一个包含数据的异步 API 调用。您希望处理来自 API 调用的数据,但首先需要确保数据已返回。.

解决这个问题的主要方法是使用回调函数。回调函数没有特殊的语法,它本质上就是一个作为参数传递给另一个函数的函数。接受另一个函数作为参数的函数被称为高阶函数。根据这个定义,任何函数只要作为参数传递,就可以成为被调用函数。电话通话本身并非异步的,但可以用于异步目的。.

以下是一个高阶函数和回调函数的语法代码示例:

// A function
function fn() {
console.log('Just a function')
}

// A function that takes another function as an argument
function higherOrderFunction(callback) {
// When you call a function that is passed as an argument, it is referred to as a callback
callback()
}

// Passing a function
higherOrderFunction(fn)

在这段代码中,你定义了一个函数 fn,定义了一个接受回调函数作为参数的函数 aboveOrderFunction,并将 fn 作为回调传递给 aboveOrderFunction。.

运行此代码将实现以下目标:

Output
Just a function

让我们回到第一个、第二个和第三个带有 setTimeout 的函数。以下是目前为止的代码:

function first() {
console.log(1)
}

function second() {
setTimeout(() => {
console.log(2)
}, 0)
}

function third() {
console.log(3)
}

任务是让第三个函数始终延迟执行,直到第二个函数中的异步操作完成。这就需要用到回调函数。与其在执行层级最高时同时执行第一个、第二个和第三个函数,不如将第三个函数作为参数传递给第二个函数。第二个函数会在异步操作完成后执行回调函数。.

以下是三个使用回调函数实现的函数:

// Define three functions
function first() {
console.log(1)
}

function second(callback) {
setTimeout(() => {
console.log(2)

// Execute the callback function
callback()
}, 0)
}

function third() {
console.log(3)
}

现在执行第一个和第二个步骤,然后将第三个步骤作为参数传递给第二个步骤:

first()
second(third)

执行此代码块后,您将得到以下输出:

Output
1
2
3

首先打印 1,然后计时器到期后(本例中为零秒,但您可以将其更改为任何值),打印 2,然后打印 3。一直运行直到异步 Web API (setTimeout) 完成。.

关键在于回调函数并非异步的——setTimeout 是一个异步 Web API,负责处理异步任务。回调函数仅允许你在异步任务完成时收到通知,并处理该任务的成功或失败。.

既然你已经学会了如何使用回调来执行异步任务,下一节将解释嵌套过多调用并创建“厄运金字塔”所带来的问题。.

嵌套回调和末日金字塔

回调函数是一种有效的方法,可以确保一个函数延迟执行,直到另一个函数完成并返回数据。然而,由于回调函数的嵌套特性,如果存在许多相互依赖的连续异步请求,代码就会变得非常混乱。这曾是早期 JavaScript 开发者的一大痛点,因此,包含嵌套调用的代码通常被称为“折磨金字塔”或“回调地狱”。.

以下是嵌套回调函数的演示:

function pyramidOfDoom() {
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
}, 500)
}, 2000)
}, 1000)
}

在这段代码中,每个新的 setTimeout 调用都被放置在一个高阶函数内部,从而形成一个层层递进的调用金字塔。运行这段代码将产生以下结果:

Output
1
2
3

在实际应用中,对于真正的异步代码,情况会变得复杂得多。你可能需要处理异步代码中的错误,并将每个响应中的一些数据传递给下一个请求。使用回调函数来实现这一点会使你的代码更难阅读和维护。.

这里有一个更贴近现实的“死亡金字塔”的可操作示例,你可以尝试一下:

// Example asynchronous function
function asynchronousRequest(args, callback) {
// Throw an error if no arguments are passed
if (!args) {
return callback(new Error('Whoa! Something went wrong.'))
} else {
return setTimeout(
// Just adding in a random number so it seems like the contrived asynchronous function
// returned different data
() => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
500,
)
}
}
// Nested asynchronous requests
function callbackHell() {
asynchronousRequest('First', function first(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest('Second', function second(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
asynchronousRequest(null, function third(error, response) {
if (error) {
console.log(error)
return
}
console.log(response.body)
})
})
})
}
// Execute 
callbackHell()

在这段代码中,你必须考虑每个函数可能出现的响应和可能出现的错误,这使得 callbackHell 函数在视觉上令人困惑。.

运行这段代码将得到以下结果:

Output
First 9
Second 3
Error: Whoa! Something went wrong.
at asynchronousRequest (<anonymous>:4:21)
at second (<anonymous>:29:7)
at <anonymous>:9:13

结果

这种管理异步代码的方法比较复杂。因此,ES6 引入了 Promise 的概念。下一节将重点介绍 Promise。.

一条评论
发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

您可能也喜欢