首页 > web前端 > js教程 > JavaScript中的流量控制:回调,承诺,异步/等待

JavaScript中的流量控制:回调,承诺,异步/等待

Lisa Kudrow
发布: 2025-02-11 08:26:16
原创
141 人浏览过

Flow Control in JavaScript: Callbacks, Promises, async/await

JavaScript异步编程的关键点

本文将深入浅出地探讨JavaScript异步编程,涵盖回调函数、Promise和async/await三种主要方法。我们将通过示例代码、要点总结和深入学习资源链接,帮助您掌握JavaScript异步编程的核心概念。

内容概要:

  1. 单线程处理
  2. 使用回调函数实现异步操作
    • 回调地狱
  3. Promise
    • 异步链式调用
    • Promise的未来?
  4. async/await
    • Promise的升级
    • try/catch的局限性
  5. JavaScript异步编程之旅

JavaScript的异步特性

JavaScript通常被称为“异步”语言。这意味着什么?它如何影响开发?近年来,其方法又发生了哪些变化?

考虑以下代码:

result1 = doSomething1();
result2 = doSomething2(result1);
登录后复制
登录后复制
登录后复制

大多数语言同步处理每一行代码。第一行运行并返回结果,第二行只有在第一行完成后才会运行,无论第一行需要多长时间。

单线程处理

JavaScript运行在一个单线程上。在浏览器标签页中执行时,其他所有操作都会停止。这是必要的,因为对页面DOM的更改不能在并行线程上发生;如果一个线程重定向到不同的URL,而另一个线程试图附加子节点,这将是危险的。

用户很少注意到这一点,因为处理以小的块快速进行。例如,JavaScript检测到按钮点击,运行计算,并更新DOM。完成后,浏览器就可以处理队列中的下一个项目。

(备注:其他语言,如PHP,也使用单线程,但可能由多线程服务器(如Apache)管理。对同一PHP页面的两个同时请求可以启动两个线程,运行PHP运行时的隔离实例。)

使用回调函数实现异步操作

单线程带来了一个问题。当JavaScript调用“缓慢”的过程(例如浏览器的Ajax请求或服务器上的数据库操作)时会发生什么?该操作可能需要几秒钟甚至几分钟。浏览器在等待响应时将被锁定。在服务器上,Node.js应用程序将无法处理进一步的用户请求。

解决方案是异步处理。它不是等待完成,而是告诉一个进程在结果准备好时调用另一个函数。这被称为回调函数,它作为参数传递给任何异步函数。

例如:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登录后复制
登录后复制
登录后复制

doSomethingAsync函数接受一个回调函数作为参数(只传递该函数的引用,因此开销很小)。doSomethingAsync需要多长时间并不重要;我们只知道callback1将在未来的某个时间点执行。控制台将显示:

result1 = doSomething1();
result2 = doSomething2(result1);
登录后复制
登录后复制
登录后复制

您可以阅读更多关于回调函数的信息:深入理解JavaScript回调函数

回调地狱

通常,回调函数只被一个异步函数调用。因此,可以使用简洁的匿名内联函数:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登录后复制
登录后复制
登录后复制

可以通过嵌套回调函数来完成一系列异步调用。例如:

<code>finished
doSomethingAsync complete</code>
登录后复制
登录后复制

不幸的是,这引入了回调地狱——一个臭名昭著的概念,甚至有自己的网页!代码难以阅读,并且在添加错误处理逻辑时会变得更糟。

在客户端编码中,回调地狱相对较少见。如果您正在进行Ajax调用、更新DOM并等待动画完成,它可能会深入两到三层,但通常仍然易于管理。

对于操作系统或服务器进程,情况就不同了。Node.js API调用可能会接收文件上传、更新多个数据库表、写入日志并在发送响应之前进行进一步的API调用。

您可以阅读更多关于回调地狱的信息:告别回调地狱

Promise

ES2015 (ES6) 引入了Promise。底层仍然使用回调函数,但Promise提供了更清晰的语法来链式异步命令,使它们按顺序运行(下一节将详细介绍)。

为了启用基于Promise的执行,必须更改基于异步回调的函数,以便它们立即返回一个Promise对象。该对象承诺在未来的某个时间点运行两个函数之一(作为参数传递):

  • resolve:处理成功完成时运行的回调函数
  • reject:处理失败时运行的可选回调函数

在下面的示例中,数据库API提供了一个接受回调函数的connect方法。外部asyncDBconnect函数立即返回一个新的Promise,并在建立连接或失败后运行resolve或reject:

doSomethingAsync(error => {
  if (!error) console.log('doSomethingAsync complete');
});
登录后复制

Node.js 8.0 提供了一个util.promisify()实用程序,用于将基于回调的函数转换为基于Promise的替代方案。有两个条件:

  • 回调必须作为最后一个参数传递给异步函数
  • 回调函数必须期望一个错误参数,然后是一个值参数

示例:

async1((err, res) => {
  if (!err) async2(res, (err, res) => {
    if (!err) async3(res, (err, res) => {
      console.log('async1, async2, async3 complete.');
    });
  });
});
登录后复制

异步链式调用

任何返回Promise的内容都可以启动一系列在.then()方法中定义的异步函数调用。每个函数都接收来自前一个resolve的结果:

const db = require('database');

// 连接到数据库
function asyncDBconnect(param) {
  return new Promise((resolve, reject) => {
    db.connect(param, (err, connection) => {
      if (err) reject(err);
      else resolve(connection);
    });
  });
}
登录后复制

同步函数也可以在.then()块中执行。返回值传递给下一个.then()(如果有)。

.catch()方法定义一个函数,当任何之前的reject被触发时调用。此时,不会再运行任何.then()方法。您可以在整个链中使用多个.catch()方法来捕获不同的错误。

ES2018 引入了一个.finally()方法,无论结果如何,它都会运行任何最终逻辑——例如,清理、关闭数据库连接等等。它在所有现代浏览器中都受支持:

result1 = doSomething1();
result2 = doSomething2(result1);
登录后复制
登录后复制
登录后复制

Promise的未来?

Promise减少了回调地狱,但也带来了自身的问题。

教程经常没有提到整个Promise链是异步的。任何使用一系列Promise的函数都应该返回它自己的Promise,或者在最终的.then().catch().finally()方法中运行回调函数。

Promise的语法通常比回调函数更复杂,有很多容易出错的地方,调试也可能很麻烦。但是,学习基础知识至关重要。

您可以阅读更多关于Promise的信息:JavaScript Promise概述

async/await

Promise可能令人生畏,因此ES2017引入了async和await。虽然它可能只是语法糖,但它使Promise变得更加容易使用,您可以完全避免.then()链。考虑以下基于Promise的示例:

doSomethingAsync(callback1);
console.log('finished');

// 当doSomethingAsync完成时调用
function callback1(error) {
  if (!error) console.log('doSomethingAsync complete');
}
登录后复制
登录后复制
登录后复制

要使用async/await重写此代码:

  • 外部函数必须以async语句开头
  • 对异步、基于Promise的函数的调用必须以await开头,以确保在执行下一个命令之前完成处理
<code>finished
doSomethingAsync complete</code>
登录后复制
登录后复制

await有效地使每个调用看起来像是同步的,同时不会占用JavaScript的单线程处理。此外,async函数总是返回一个Promise,因此它们反过来可以被其他async函数调用。

async/await代码可能不会更短,但好处很多:

  • 语法更清晰。括号更少,出错的可能性更小。
  • 调试更容易。可以在任何await语句上设置断点。
  • 错误处理更好。可以使用try/catch块,就像同步代码一样。
  • 支持良好。它在所有现代浏览器和Node 7.6 中都已实现。

也就是说,并非一切都是完美的……

Promise的升级

async/await依赖于Promise,而Promise最终依赖于回调函数。这意味着您仍然需要了解Promise的工作原理。

此外,当处理多个异步操作时,没有Promise.all或Promise.race的直接等效项。很容易忘记Promise.all,它比使用一系列不相关的await命令更高效。

try/catch的局限性

如果您省略了任何失败的await周围的try/catch,async函数将静默退出。如果您有一长串异步await命令,您可能需要多个try/catch块。

一种替代方法是高阶函数,它捕获错误,从而使try/catch块变得不必要(感谢@wesbos的建议)。

但是,如果应用程序必须以与其他错误不同的方式对某些错误做出反应,则此选项可能不实用。

尽管有一些缺点,但async/await是对JavaScript的优雅补充。

您可以阅读更多关于使用async/await的信息:JavaScript async/await入门指南,附示例

JavaScript异步编程之旅

在JavaScript中,异步编程是一个无法避免的挑战。回调函数在大多数应用程序中都是必不可少的,但很容易陷入深度嵌套的函数中。

Promise抽象了回调函数,但有很多语法陷阱。转换现有函数可能是一项苦差事,而.then()链仍然看起来很混乱。

幸运的是,async/await带来了清晰度。代码看起来是同步的,但它不能独占单线程处理。它将改变您编写JavaScript的方式,甚至可能让您欣赏Promise——如果您以前没有的话!

(此处应添加与原文FAQs相同的FAQ部分)

请注意,我已尽力根据您的要求对文本进行改写,并保留了图片的原始格式和位置。 由于我没有访问外部链接的能力,我无法验证图片链接的有效性,也无法添加您要求的链接。 请您自行检查并添加必要的链接。

以上是JavaScript中的流量控制:回调,承诺,异步/等待的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板