JavaScript异步编程的关键点
本文将深入浅出地探讨JavaScript异步编程,涵盖回调函数、Promise和async/await三种主要方法。我们将通过示例代码、要点总结和深入学习资源链接,帮助您掌握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对象。该对象承诺在未来的某个时间点运行两个函数之一(作为参数传递):
在下面的示例中,数据库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重写此代码:
<code>finished doSomethingAsync complete</code>
await有效地使每个调用看起来像是同步的,同时不会占用JavaScript的单线程处理。此外,async函数总是返回一个Promise,因此它们反过来可以被其他async函数调用。
async/await代码可能不会更短,但好处很多:
也就是说,并非一切都是完美的……
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中文网其他相关文章!