回调和承诺的简单解释

2025-06-07

回调和承诺的简单解释

封面图片来源

回调?

// caller
function foo(callback) {
  callback('world');
}

// callback function
function myCallback(name) {
  console.log(`Hello ${name}`); // "hello world"
}

// pass callback to caller
foo(myCallback);

要理解回调,首先需要了解NodeJS总体上是如何运行代码的。NodeJS 中的一切都由“事件循环”控制,因为 NodeJS 的核心是一个单一、巨大且非常复杂的循环。

在 NodeJS 中运行代码时,每一行代码都会由底层的 V8(JavaScript 引擎)解释执行。基本的语言操作,例如数学运算和字符串操作,会立即将结果返回给调用者。但其他操作,例如网络请求、读写文件以及访问系统硬件,并不会立即执行,而是被添加到事件循环的“调用栈”中。事件循环会持续按照 LIFO(后进先出)的顺序执行可用的任务。如果某个任务强制事件循环在处理其他事件循环元素之前完成其计算,我们就称它“阻塞”了事件循环。此外,我们将这种阻塞直至完成的任务称为同步任务

如果你想进一步了解事件循环,这篇文章太棒了。真的,太棒了!

还有一种可以注册到事件循环的任务类型,即异步任务。正如你所料,异步任务与同步任务相反,不会阻塞事件循环。相反,异步任务需要提供一个可以“回调”的函数,用于处理异步事件完成所产生的任何结果。这解释了什么是回调,但为什么需要它们呢?

为什么要回调?

想象一下,如果网站必须在浏览器中逐一加载所有资源,并且直到所有资源都加载完毕才能渲染。如果真是这样,Gmail 需要 30 多秒才能出现在我的电脑上。回调解决了这个问题,它允许 CPU 消耗极低的任务长时间运行,而不会阻塞其他任务。需要明确的是,这不是并行,因为两件事不会同时发生(NodeJS 是单线程的)。


来源

大多数核心 NodeJS API(例如filesystem)都是异步实现的,以最大程度地减少事件循环的阻塞。如果您还不清楚,我发现,概括何时需要回调的最佳方法如下:

如果代码与另一个系统交互,并且该系统无法保证其可靠性(文件系统、网络、gpu),则可能需要回调。

例如,如果您向 stripe.com 发送 POST 请求,您无法保证 stripe.com 的响应速度(甚至完全响应)。为了解决这种不可靠性,您可以以非阻塞方式发送 POST 请求,并注册一个回调函数,该回调函数将在 stripe.com 服务器响应时调用。由于该 stripe.com 请求是异步的,您可以向 AWS S3 服务(例如)发送并发(而非并行)请求,从而大幅缩短应用程序的加载时间。

为什么回调不好

来源

随着时间的推移,人们开始对回调感到沮丧。理论上,回调是延迟代码执行的绝佳解决方案。不幸的是,实际使用中鼓励深度回调嵌套来处理嵌套事件(由另一个异步事件引发的异步事件)。

显然,对于字符串操作之类的操作,你不需要回调。这只是为了保持简洁而特意设计的例子。

// caller
function foo(callback) {
  callback('world', myNestedCallback);
}

// inner inner callback
function myNestedNestedCallback(name, callback) {
  console.log(`Hello ${name}`);
  // Prints "Hello First Name: Mr. world"
}

// inner callback
function myNestedCallback(name, callback) {
  callback(`First Name: ${name}`);
}

// callback function
function myCallback(name, callback) {
  callback(`Mr. ${name}`, myNestedNestedCallback);
}

// pass callback to caller
foo(myCallback);

这被称为“回调地狱”,因为当代码嵌套在多个回调中时,会变得非常混乱。确定当前作用域和可用变量通常变得非常困难。

图片来源

当你需要加载多个内容且不关心它们的处理顺序时,回调是可行的,但当你需要编写有序的、连续的代码时,回调就不太合适了。大多数情况下,人们使用深层回调链来人为地制造连续的代码。我们需要一个解决方案,既不阻塞事件循环,又允许代码按顺序执行,而无需过度嵌套。

承诺

不管你听说过什么,Promise其实只是一个花哨的回调函数。它实际上是一个带有定义良好的 API 的回调函数包装器。Promise API 允许你查询底层异步事件的状态,并提供了方法允许你注册逻辑来处理底层异步事件完成时产生的结果或错误。Promise 主要解决了嵌套问题,因为它们将代码变成如下所示:

// caller
function foo(callback) {
  callback('world', myNestedCallback);
}

// inner inner callback
function myNestedNestedCallback(name, callback) {
  console.log(`Hello ${name}`);
  // Prints "Hello First Name: Mr. world"
}

// inner callback
function myNestedCallback(name, callback) {
  callback(`First Name: ${name}`);
}

// callback function
function myCallback(name, callback) {
  callback(`Mr. ${name}`, myNestedNestedCallback);
}

// pass callback to caller
foo(myCallback);

变成这样:

function myNestedNestedCallback(name) {
  return new Promise((resolve, reject) => {
    console.log(`Hello ${name}`); // Prints "Hello First Name: Mr. world"
  })
}

function myNestedCallback(name) {
  return new Promise((resolve, reject) => {
    resolve(`First Name: ${name}`);
  });
}


function myCallback(name) {
  return new Promise((resolve, reject) => {
    resolve(`Mr. ${name}`);
  });
}

myCallback('world').then(myNestedCallback).then(myNestedNestedCallback);

如果您想将当前使用回调的代码转换为使用 Promise 的等效代码,这是一个很好的参考:

// callback way
function addCallback(a, b, callback) {
  callback(a + b);
}

// promise way
function addPromise(a, b) {
  return new Promise((resolve, reject) => {
    resolve(a + b);
  });
}

如果你正在与基于回调的 API 进行交互,并希望在外部将其转换为 Promise,

// signature
function makeHTTPRequest(url, method, callback) {}


const convertedToPromise = new Promise((resolve, reject) => {
  makeHTTPRequest('google.com', 'GET', (body, err) => {
    if (err) {
      return reject(err);
    }
    return resolve(body);
  });
});

convertedToPromise.then((res) => console.log(res)); // prints response from google.com

许多回调还可以通过NodeJS 中的包自动转换为其“承诺”的版本util

const { promisify } = require('util');

function addCallback(a, b, callback) {
  callback(a + b);
}

const asyncAdd = promisify(addCallback);
asyncAdd(3, 6).then((res) => console.log(res)); // "9"

异步等待

最后,我们有asyncawait。它们类似于 Promise 和回调之间的关系,async并且await实际上只是 Promise 的一种使用方式。async&await提供了一种语法,可以编写类似于原生同步代码的 Promise 代码,这通常会使 JavaScript 代码更具可读性和可维护性。在函数上使用async标识符时,它等效于以下 Promise 代码。

// async version
async function add(a, b) {
  return a + b; // really returns a Promise under the hood
}

// equivalent code but promise way
function addPromise(a, b) {
  return new Promise((resolve, reject) => {
    resolve(a + b);
  });
}

add(1, 2).then((res) => console.log(res)); // "3"
addPromise(1, 2).then((res) => console.log(res)); // "3"

事实上,所有async函数都会返回一个完整的 Promise 对象。它await为方法提供了额外的功能async。在调用异步函数之前使用 await 时,这意味着代码应该直接将异步结果返回到表达式的左侧,而不是使用显式的异步任务。这允许你编写有序的同步风格代码,同时享受异步求值的所有好处。如果仍然不明白,以下是awaitPromises 中 is 的等价形式。

async function add(a, b) {
  return a + b;
}

async function main() {
  const sum = await add(6, 4);
  console.log(sum); // "10" 
}

请记住,await这只是一个 hack,.then()允许代码在不嵌套的情况下进行样式设置。上面的代码和下面的代码在功能上没有任何区别。

function addPromise(a, b) {
  return new Promise((resolve, reject) => {
    resolve(a + b);
  });
}

addPromise(6, 4).then((res => console.log(res))); // "10"

结论

我希望这篇文章能帮助那些还在努力理解回调和 Promise 核心机制的人。在大多数情况下,它们只是一些语法糖,并没有那么复杂。

如果您仍在努力理解并行、异步和并发等基本概念,我推荐您阅读我最近撰写的有关这些主题的文章

我的博客

文章来源:https://dev.to/taillogs/callbacks-and-promises-simply-explained-3dkd
PREV
MyOS
NEXT
你的 Bash 脚本太垃圾了,用其他语言吧