JavaScript 回调处理并发请求

191 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情

使用 JavaScript 回调处理并发请求

并发是项目开发中的一个常见概念。它是同时执行多个任务的能力。并发编程一开始可能看起来很复杂,但它可以帮助我们增强用户体验,并使我们的项目更具动态性和交互性。

本文将介绍我们如何使用纯 JavaScript 回调处理并发请求,以及它们的优点、缺点、适用性和最佳实践。

什么是回调?

在 JavaScript 中编写并发代码的核心是 回调函数和 http 请求调用。

作为另一个函数的参数并在外部函数中调用以完成任务或事件之后的函数称为回调函数。

因此,回调函数是一段代码,只有在另一段代码完成后才能执行。这个是重点

根据这个定义,任何没有作为参数传递的特定语法的函数都可以是回调函数,并且这些函数本质上不是并发的。但是,我们可以应用回调函数来处理 JavaScript 中的并发性。

以下是实现回调函数的方式。

// A function.
function foo() {
    console.log('foo')
}
// HOF 高阶函数,将函数作为参数
function higherOrderFunction(callback) {
    callback()
}_

higherOrderFunction(foo)

输出A

现在,让我们看看如何使用回调来处理并发请求。

在以下示例中看到使用回调处理并发请求的演示。

setTimeout() 是一个内置的 JavaScript 异步函数,它在指定的时间后运行一个函数或分析一个表达式。它有两个参数:一个回调函数和一个毫秒长的延迟,第三个参数不常用。

下面示例中的setTimeout(),它使用回调函数调用setTimeout(),将 printOutput() 附加到结果对象。我们将延迟设置为 500 毫秒。

var delay = 500;
function printOutput(result) {
    var runtime = Date.now() - result.start;
    console.log('execution', result.n, 'completed in', runtime, 'delay', result.delay);
    return runtime;
}
var result = { n:0, start:Date.now(), delay:delay };
var out = setTimeout( () => printOutput(result), delay );

执行前面的代码时,printOutput() 函数将返回一条消息,其中包括代码执行和终止之间所用的时间,以及 setTimeout() 函数设置的延迟。就像在下面的示例输出中一样,这两个持续时间不太可能相等。此外,printOutput() 方法返回准确的运行时间(以毫秒为单位)。

execution 0 completed in 520 ms delay 505

现在,通过封装 setTimeout() 调用和使用回调来创建自定义并发函数。并发行为基于包含不必要的随机延迟。

以下代码中的 asyncFn() 是一个异步函数,它采用整数n。调用时,该函数会创建一个随机延迟,并在该时间过去后向终端发布一条消息。数字n是一个标签,它记录了 asyncFn() 被调用了多少次。当然,asyncFn() 立即将值返回为undefined。 因此,结果的值是 undefined 的。

function asyncFn( n ){
    const delay = Math.floor( 100 + Math.random() * 900 );
    function printOutput( result ){
        var runtime = Date.now() - result.start;
        console.log( 'execution', n, 'completed in', runtime, 'ms after a delay of', delay );
        return runtime;
    }
    var result = { n:n, start:Date.now(), delay:delay }
    setTimeout( () => printOutput( result ), delay );
}
for( var rank = 0; rank < 5; rank++) {
    var out = asyncFn( rank );
}

当循环执行时,这段代码的异步特性是可见的。这五个顺序调用提供的输出与它们执行的顺序不匹配,尽管事实上asyncFn()被调用了五次,顺序为 0 到 5。此外,延迟时间和实际完成时间之间的差距在调用之间波动。

对于先前代码的一次执行,输出将如下所示。

execution 2 completed in 170 ms after a delay of 166
execution 0 completed in 185 ms after a delay of 184
execution 1 completed in 481 ms after a delay of 476
execution 3 completed in 519 ms after a delay of 518
execution 4 completed in 794 ms after a delay of 792

尽管它显示了并发性,但代码效率低下,因为asyncFn()

  • 无法将计算结果传回给调用者。
  • 我们这里需要把计算结果。

函数应该向调用者公开它们的输出,并让调用者决定如何处理计算的值。回调函数提供了解决这两个设计问题的解决方案。

现在让我们将 asyncFn() 重写为一个接受两个参数的函数:一个整数值 n 和一个回调函数 cb。回调函数接受一个参数,即 asyncFn 的计算输出,并在 100 到 999 毫秒的随机延迟后执行。因此允许客户端代码设计自己的回调函数:

  • 通过提供的参数接收准确的输出。
  • 以希望的任何方式处理。

在下面的示例中,回调函数在上下文中声明,允许通过回调的显式参数结果访问计算的输出。

function asyncFn( n, cb ) {
    var delay = Math.floor( 100 + Math.random() * 900 );
    var result = { n:n, start:Date.now(), delay:delay };
    setTimeout( () => cb( result ), delay );
}
for( var rank = 0; rank < 5 ; rank++){
    function printOutput( result ) {
        var runtime = Date.now() - result.start;
        // 注意这里
         console.log( 'execution', result.n, 'completed in', runtime, 'ms after a delay of', result.delay); 
         return runtime; 
    }
    var out= asyncFn( rank, printOutput);
}

所以,现在应该了解通过 JavaScript 中的回调实现并发。

尽管前面示例中的回调会立即执行,但大多数 JavaScript 回调都与事件相关联,例如计时器、API 调用或读取文件。如果正确使用回调,它们有助于使代码更易于管理。

回调的优点

  • 使代码尽可能简单(减少重复)。
  • 改进抽象,以便可以使用更通用的函数来执行广泛的功能(例如,求和、乘积)。
  • 增强代码的可读性和可维护性。

回调的缺点

  • 回调函数有一定的局限性,尽管它们提供了一种简单的方法来处理 JavaScript 中的异步。
  • 如果未正确使用回调,则可能会创建回调地狱。
  • 错误处理比较复杂。
  • 不能在 return 语句中使用 throw 关键字或返回值。

总结

并发代码比顺序代码更好,因为它是非阻塞的,可以同时容纳多个用户或请求,并且问题最少。本文演示了如何使用 JavaScript 回调处理并发。我们可以决定应该在哪里使用回调来处理并发请求,而不会陷入回调地狱。