×

回归基础:什么是 JavaScript 中的回调?

作者:Terry2022.11.22来源:Web前端之家浏览:3003评论:0
关键词:JavaScript

500.jpg

当你开始学习 JavaScript 时,不久你就会听到“回调函数”这个词。回调是 JavaScript 执行模型不可或缺的一部分,了解它们是什么以及它们如何工作很重要。

什么是 JavaScript 回调?

在 JavaScript 中,回调是作为参数传递给第二个函数的函数。接收回调的函数决定是否以及何时执行回调:

function myFunction(callback) {
    // 1. Do something
    // 2. Then execute the callback
    callback()
}
 
  function myCallback() {
    // Do something else
}
 
myFunction(myCallback);

在上面的例子中,我们有两个函数:myFunctionmyCallback。顾名思义,myCallback用作回调函数,我们将它myFunction作为参数传递给。myFunction然后可以在准备好时执行回调。

许多博客文章会说回调之所以称为回调,是因为您告诉某些函数在准备好回答时给您回电。一个不太容易混淆的名字是“callafter”:也就是说,在你完成所有其他事情之后调用这个函数。

为什么我们需要回调函数?

你会经常听到人们说 JavaScript 是单线程的。这意味着它一次只能做一件事。当执行缓慢的操作时——例如从远程 API 获取数据——这可能会出现问题。如果您的程序在返回数据之前冻结,那将不会是很好的用户体验。

JavaScript 避免这种瓶颈的方法之一是使用回调。我们可以将第二个函数作为参数传递给负责数据获取的函数。然后启动数据获取请求,但 JavaScript 解释器不会等待响应,而是继续执行程序的其余部分。当从 API 收到响应时,将执行回调函数并对结果执行某些操作:

function fetchData(url, cb) {
    // 1. Make API request to url
    // 2. If response successful, execute callback
    cb(res);
  }
 
  function callback(res) {
    // Do something with results
  }
 
  // Do something
  fetchData('https://sitepoint.com', callback);
  // Do something else

JavaScript 是一种事件驱动的语言

您还会听到人们说 JavaScript 是一种事件驱动的语言。这意味着它可以侦听和响应事件,同时继续执行更多代码并且不会阻塞其单个线程。

它是如何做到的?你猜对了:回调。

想象一下,如果您的程序将一个事件侦听器附加到一个按钮,然后坐在那里等待某人单击该按钮,同时拒绝执行任何其他操作。那可不好!

使用回调,我们可以指定应运行特定代码块以响应特定事件:

function handleClick() {
    // Do something (e.g. validate a form)
    // in response to the user clicking a button
  }
 
  document.querySelector('button').addEventListener('click', handleClick);

在上面的示例中,该handleClick函数是一个回调,它是为响应网页上发生的操作(单击按钮)而执行的。

使用这种方法,我们可以根据需要对任意数量的事件做出反应,同时让 JavaScript 解释器自由地继续执行它需要做的任何其他事情。

一等函数和高阶函数

在学习回调时,您可能会遇到更多的流行语是“一等函数”和“高阶函数”。这些听起来很可怕,但实际上并非如此。

当我们说 JavaScript 支持一等函数时,这意味着我们可以像对待常规值一样对待函数。我们可以将它们存储在一个变量中,我们可以从另一个函数返回它们,而且,正如我们已经看到的,我们可以将它们作为参数传递。

至于高阶函数,这些只是将函数作为参数或返回函数作为结果的函数。有几个本机 JavaScript 函数也是高阶函数,例如setTimeout. 让我们用它来演示如何创建和运行回调。

如何创建回调函数

模式与上面相同:创建一个回调函数并将其作为参数传递给高阶函数:

function greet() {
    console.log('Hello, World!');
}
 
setTimeout(greet, 1000);

该函数延迟一秒setTimeout执行函数并记录“Hello, World!” greet到控制台。

我们还可以让它稍微复杂一点,并向greet函数传递一个需要问候的人的名字:

function greet(name) {
    console.log(`Hello, ${name}!`);
}
 
setTimeout(() => greet('Jim'), 1000);

请注意,我们使用了箭头函数来包装对greet. 如果我们没有这样做,该函数将立即执行,而不是在延迟之后执行。

如您所见,在 JavaScript 中有多种创建回调的方法,这让我们很好地进入下一节。

不同种类的回调函数

部分归功于 JavaScript 对一等函数的支持,在 JavaScript 中有多种声明函数的方式,因此也有多种在回调中使用它们的方式。

现在让我们看看这些并考虑它们的优点和缺点。

匿名函数

到目前为止,我们一直在命名我们的函数。这通常被认为是好的做法,但绝不是强制性的。考虑以下使用回调函数验证某些表单输入的示例:

document.querySelector('form').addEventListener('submit', function(e)  {
    e.preventDefault();
    // Do some data validation
    // If everything looks ok, then...
    this.submit();
});

如您所见,回调函数未命名。没有名称的函数定义称为匿名函数。匿名函数在只在一个地方被调用的短脚本中使用得很好。而且,由于它们被声明为内联的,因此它们也可以访问其父级的范围。

箭头函数

ES6 引入了箭头函数。由于它们简洁的语法,并且因为它们具有隐式返回值,所以它们通常用于执行简单的单行代码,例如在以下示例中,它从数组中过滤重复值:

const arr = [1, 2, 2, 3, 4, 5, 5];
const unique = arr.filter((el, i) => arr.indexOf(el) === i);
// [1, 2, 3, 4, 5]

但是请注意,它们不绑定自己的this值,而是从它们的父范围继承它。这意味着,在前面的示例中,我们将无法使用箭头函数来提交表单:

document.querySelector('form').addEventListener('submit', (e) => {
    ...
    // Uncaught TypeError: this.submit is not a function
    // `this` points to the window object, not to the form
    this.submit();
  });

箭头函数是近年来我最喜欢的 JavaScript 新增功能之一,它们绝对是开发人员应该熟悉的东西。如果您想了解有关箭头函数的更多信息,请查看我们的 JavaScript 中的箭头函数:如何使用 Fat & Concise 语法教程。

命名函数

在 JavaScript 中创建命名函数主要有两种方式:函数表达式和函数声明。两者都可以与回调一起使用。

函数声明涉及使用关键字创建函数function并为其命名:

function myCallback() {... }
setTimeout(myCallback, 1000);

函数表达式涉及创建一个函数并将其分配给一个变量:

const myCallback = function() { ... };
setTimeout(myCallback, 1000);

或者:

const myCallback = () => { ... };
setTimeout(myCallback, 1000);

我们还可以标记使用function关键字声明的匿名函数:

setTimeout(function myCallback()  { ... }, 1000);

以这种方式命名或标记回调函数的优点是它有助于调试。让我们的函数抛出一个错误:

setTimeout(function myCallback() { throw new Error('Boom!'); }, 1000);

// Uncaught Error: Boom!
// myCallback  file:///home/jim/Desktop/index.js:18
// setTimeout handler*  file:///home/jim/Desktop/index.js:18

使用命名函数,我们可以准确地看到错误发生的位置。但是,看看当我们删除名称时会发生什么:

setTimeout(function() { throw new Error('Boom!'); }, 1000);

// Uncaught Error: Boom!
// <anonymous>  file:///home/jim/Desktop/index.js:18
// setTimeout handler*  file:///home/jim/Desktop/index.js:18

在这个独立的小示例中,这没什么大不了的,但是随着代码库的增长,这是需要注意的事情。甚至有一个ESLint 规则来强制执行此行为。

JavaScript 回调函数的常见用例

JavaScript 回调函数的用例广泛多样。正如我们所见,它们在处理异步代码(如 Ajax 请求)和响应事件(如表单提交)时非常有用。现在让我们再看几个可以找到回调的地方。

数组方法

遇到回调的另一个地方是在 JavaScript 中使用数组方法时。随着您在编程之旅中的进步,您将越来越多地这样做。例如,假设你想对一个数组中的所有数字求和,考虑这个简单的实现:

const arr = [1, 2, 3, 4, 5];
let tot = 0;
for(let i=0; i<arr.length; i++) {
  tot += arr[i];
}
console.log(tot); //15

虽然这可行,但更简洁的实现可能会使用Array.reduce它,您猜对了,它使用回调对数组中的所有元素执行操作:

const arr = [1, 2, 3, 4, 5];
const tot = arr.reduce((acc, el) => acc + el);
console.log(tot);
// 15

Node.js

还应该注意的是,Node.js及其整个生态系统严重依赖基于回调的代码。例如,这里是规范的 Hello, World! 的节点版本!例子:

const http = require('http');

http.createServer((request, response) => {
  response.writeHead(200);
  response.end('Hello, World!');
}).listen(3000);

console.log('Server running on http://localhost:3000');

无论您是否曾经使用过 Node,这段代码现在应该很容易理解。本质上,我们需要 Node 的http模块并调用它的createServer方法,我们将匿名箭头函数传递给它。任何时候 Node 在端口 3000 上收到请求时都会调用此函数,它将以 200 状态和文本“Hello, World!”作为响应。

Node 还实现了一种称为错误优先回调的模式。这意味着回调的第一个参数是为错误对象保留的,而回调的第二个参数是为任何成功的响应数据保留的。

下面是 Node 文档中的一个示例,展示了如何读取文件:

const fs = require('fs');
fs.readFile('/etc/hosts', 'utf8', function (err, data) {
  if (err) {
    return console.log(err);
  }
  console.log(data);
});

我们不想在本教程中深入探讨 Node,但希望这种代码现在应该更容易阅读。

同步与异步回调

回调是同步执行还是异步执行取决于调用它的函数。让我们看几个例子。

同步回调函数

当代码是同步的时,它从上到下逐行运行。操作一个接一个地发生,每个操作都等待前一个操作完成。我们已经在Array.reduce上面的函数中看到了一个同步回调的例子。

为了进一步说明这一点,这里有一个演示,它同时使用Array.mapArray.reduce来计算逗号分隔数字列表中的最高数字:

<!DOCTYPE html>
<html>
<head>
<title>Numbers</title>
<meta charset="utf-8">
<style>
  form{
    max-width: 600px;
    padding: 15px;
  }
</style>
</head>
<body>
  <form>
    <div class="mb-3">
      <p id="result"></p>
      <label for="numbers" class="form-label">Enter a list of comma separated numbers</label>
      <input type="text" required class="form-control" id="numbers" placeholder="1,2,3,4,5">
    </div>
   
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
  <script>
    const form = document.querySelector('form');
    const input = document.querySelector('input');
    const res = document.querySelector('p');

    form.addEventListener('submit', (e) => {
      e.preventDefault();
      const highest = input.value
        .replace(/\s+/, '')
        .split(',')
        .map((el) => Number(el))
        .reduce((acc,val) => (acc > val) ? acc : val);

      res.innerText = `The highest number is ${highest}`
    });
  </script>
</body>
</html>

主要动作发生在这里:

const highest = input.value
  .replace(/\s+/, '')
  .split(',')
  .map((el) => Number(el))
  .reduce((acc,val) => (acc > val) ? acc : val);

从上到下,我们执行以下操作:

  • 获取用户的输入

  • 删除任何空格

  • 在逗号处拆分输入,从而创建一个字符串数组

  • 使用回调将字符串转换为数字来映射数组的每个元素

  • 用于reduce迭代数字数组以确定最大的

为什么不尝试一下 CodePen 上的代码,并尝试更改回调以产生不同的结果(例如找到最小的数字,或所有奇数,等等)。

异步回调函数

与同步代码相比,异步JavaScript 代码不会从上到下逐行运行。相反,异步操作将注册一个回调函数,一旦完成就执行。这意味着 JavaScript 解释器不必等待异步操作完成,而是可以在运行时继续执行其他任务。

异步函数的主要示例之一是从远程 API 获取数据。现在让我们看一个例子,了解它是如何使用回调的。

<!DOCTYPE html>
<html>
<head>
<title>Numbers</title>
<meta charset="utf-8">
<style>
  .users {
    padding: 15px;
  }

  ul {
    margin-top: 15px;
  }
</style>
</head>
<body>
  <div class="mb-3 users">
    <button type="submit" class="btn btn-primary">Fetch Users</button>
    <ul id="result"></ul>
  </div>
  <script>
    const button = document.querySelector('button');
    const ul = document.querySelector('ul');

    button.addEventListener('click', (e) => {
      e.preventDefault();

      fetch('https://jsonplaceholder.typicode.com/users')
        .then(response => response.json())
        .then(json => {
          const names = json.map(user => user.name);
          names.forEach(name => {
            const li = document.createElement('li');
            li.textContent = name;
            ul.appendChild(li);
          });
        })
    });
  </script>
</body>
</html>

主要动作发生在这里:

fetch('https://jsonplaceholder.typicode.com/users')
  .then(response => response.json())
  .then(json => {
    const names = json.map(user => user.name);
    names.forEach(name => {
      const li = document.createElement('li');
      li.textContent = name;
      ul.appendChild(li);
    });
});

上例中的代码使用FetchAPI 将对虚拟用户列表的请求发送到伪造的 JSON API。服务器返回响应后,我们将运行第一个回调函数,它会尝试将该响应解析为 JSON。之后,运行我们的第二个回调函数,它构造一个用户名列表并将它们附加到列表中。请注意,在第二个回调中,我们使用另外两个嵌套回调来完成检索名称和创建列表元素的工作。

我再次鼓励您尝试一下代码。如果您查看API 文档,您可以获取和操作大量其他资源。

使用回调时的注意事项

回调在 JavaScript 中已经存在很长时间了,它们可能并不总是最适合你想要做的事情。让我们看一下需要注意的几件事。

谨防 JavaScript 回调地狱

我们在上面的代码中看到可以嵌套回调。这在处理相互依赖的异步函数时尤其常见。例如,您可能会在一个请求中获取电影列表,然后使用该电影列表获取每部电影的海报。

虽然这对于一层或两层嵌套来说没有问题,但您应该意识到这种回调策略的扩展性不佳。不久之后,您将得到凌乱且难以理解的代码:

fetch('...')
  .then(response => response.json())
  .then(json => {
    // Do some processing
    fetch('...')
      .then(response => response.json())
      .then(json => {
        // Do some more processing
        fetch('...')
          .then(response => response.json())
          .then(json => {
            // Do even processing
            fetch('...')
              .then(response => response.json())
              .then(json => {
                // Do yet more processing
              });
          });
      });
  });


更喜欢更现代的流量控制方法

虽然回调是 JavaScript 工作方式不可或缺的一部分,但该语言的最新版本添加了改进的流程控制方法。

例如,promises 并async...await提供更简洁的语法来处理上述代码。虽然超出了本文的范围,但您可以在现代 JS 中的 JavaScript Promises和流程控制概述:回调到 Async/Await 的 Promises中阅读所有相关内容。

结论

在本文中,我们研究了回调到底是什么。我们了解了 JavaScript 执行模型的基础知识、回调如何适应该模型以及为什么需要回调。我们还研究了如何创建和使用回调、不同类型的回调以及何时使用它们。您现在应该牢牢掌握在 JavaScript 中使用回调,并能够在您自己的代码中使用这些技术。

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/jsbackrol20221122.html

网友评论文明上网理性发言 已有0人参与

发表评论: