JavaScript中的流控制:回调、promises、异步/等待 在本文中,我们将深入了解如何在 JavaScript 中使用异步代码。我们将从回调开始,继续到promises,然后以更现代的async/await
. 每个部分都将提供示例代码,概述要注意的要点,并链接到更深入的资源。
JavaScript 经常声称是异步的。那是什么意思?它如何影响发展?近年来,这种方法有何变化?
考虑以下代码:
result1 = doSomething1(); result2 = doSomething2(result1);
大多数语言同步处理每一行。第一行运行并返回结果。第二行在第一行完成后运行——不管需要多长时间。
单线程处理
JavaScript 在单个处理线程上运行。在浏览器选项卡中执行时,其他一切都会停止。这是必要的,因为对页面 DOM 的更改不能发生在并行线程上;让一个线程重定向到不同的 URL 而另一个线程尝试附加子节点是很危险的。
这对用户来说很少是显而易见的,因为处理会以小块的形式快速发生。例如,JavaScript 检测按钮点击、运行计算并更新 DOM。一旦完成,浏览器就可以自由处理队列中的下一个项目。
(旁注:PHP 等其他语言也使用单线程,但可能由 Apache 等多线程服务器管理。同时对同一 PHP 页面的两个请求可以启动两个运行 PHP 运行时的独立实例的线程.)
通过回调实现异步
单线程引发了一个问题。当 JavaScript 调用浏览器中的 Ajax 请求或服务器上的数据库操作等“慢速”进程时会发生什么?该操作可能需要几秒钟——甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js 应用程序将无法处理进一步的用户请求。
解决方案是异步处理。不是等待完成,而是告诉进程在结果准备好时调用另一个函数。这称为回调,它作为参数传递给任何异步函数。
例如:
doSomethingAsync(callback1); console.log('finished'); // call when doSomethingAsync completes function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }
该doSomethingAsync
函数接受回调作为参数(仅传递对该函数的引用,因此开销很小)。花多长时间并不重要doSomethingAsync
;我们所知道的是,callback1
它将在未来的某个时间被执行。控制台将显示:
finished doSomethingAsync complete
您可以在回归基础中阅读有关回调的更多信息:什么是 JavaScript 中的回调?
回调地狱
通常,回调只会被一个异步函数调用。因此可以使用简洁的匿名内联函数:
doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });
可以通过嵌套回调函数串联完成一系列两个或多个异步调用。例如:
async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });
不幸的是,这引入了回调地狱——一个臭名昭著的概念,甚至有自己的网页!代码难以阅读,并且在添加错误处理逻辑时会变得更糟。
回调地狱在客户端编码中相对罕见。如果您正在进行 Ajax 调用、更新 DOM 并等待动画完成,它可以深入两到三级,但它通常仍可管理。
操作系统或服务器进程的情况有所不同。Node.js API 调用可以接收文件上传、更新多个数据库表、写入日志,并在发送响应之前进行进一步的 API 调用。
您可以在从回调地狱中保存中阅读有关回调地狱的更多信息。
Promises
ES2015 (ES6) 引入了promises。回调仍在表面之下使用,但 promises 提供了一种更清晰的语法,可以链接异步命令,以便它们连续运行
要启用基于 promise 的执行,必须更改基于异步回调的函数,以便它们立即返回 promise 对象。该对象promises在将来的某个时候运行两个函数之一(作为参数传递):
resolve
: 处理成功完成时运行的回调函数reject
:发生故障时运行的可选回调函数
在下面的示例中,数据库 API 提供了一个connect
接受回调函数的方法。外部asyncDBconnect
函数立即返回一个新的promises,并在连接建立或失败时resolve
运行:reject
const db = require('database'); // Connect to database function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }
Node.js 8.0+ 提供了一个util.promisify() 实用程序来将基于回调的函数转换为基于promises的替代方法。有几个条件:
- 回调必须作为最后一个参数传递给异步函数
- 回调函数必须期待一个错误后跟一个值参数
例子:
// Node.js: promisify fs.readFile const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');
异步链接
任何返回promises的东西都可以启动一系列在.then()
方法中定义的异步函数调用。每个都传递了前一个的结果resolve
:
asyncDBconnect('http://localhost:1234') .then(asyncGetSession) // passed result of asyncDBconnect .then(asyncGetUser) // passed result of asyncGetSession .then(asyncLogAccess) // passed result of asyncGetUser .then(result => { // non-asynchronous function console.log('complete'); // (passed result of asyncLogAccess) return result; // (result passed to next .then()) }) .catch(err => { // called on any reject console.log('error', err); });
同步功能也可以在.then()
块中执行。返回值传递给下一个.then()
(如果有的话)。
该方法定义了一个函数,该函数在触发.catch()
任何前一个函数时调用。reject
届时,将不再运行其他.then()
方法。您可以.catch()
在整个链中使用多种方法来捕获不同的错误。
ES2018引入了一种.finally()
方法,无论结果如何,它都会运行任何最终逻辑——例如,清理、关闭数据库连接等。所有现代浏览器都支持它:
function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // tidy-up here! }); }