JavaScript 中的流量控制:回调、承诺、异步/等待
在本文中,我们将从高层次上了解如何在 JavaScript 中使用异步代码。我们将从回调开始,继续承诺,然后以更现代的异步/等待
结束。每个部分都将提供示例代码,概述需要注意的要点,并链接到更深入的资源。
原文:Flow Control in JavaScript: Callbacks, Promises, async/await (sitepoint.com)
内容:
- 单线程处理
- 使用回调异步
- 回调地狱
- 承诺
- 异步链接
- 充满希望的未来?
- 异步/等待
- 承诺,承诺
- 尝试/捕捉丑陋
- JavaScript 之旅
JavaScript经常被声称是_异步_的。那是什么意思?它如何影响发展?近年来,这种方法发生了怎样的变化?
请考虑以下代码:
result1 = doSomething1();
result2 = doSomething2(result1);
大多数语言_同步_处理每一行。第一行运行并返回结果。第二条线在第一条线完成后运行——无论需要多长时间。
单线程处理
JavaScript 在单个处理线程上运行。在浏览器选项卡中执行时,其他所有内容都会停止。这是必要的,因为对页面 DOM 的更改不能在并行线程上进行;让一个线程重定向到不同的 URL 而另一个线程尝试附加子节点是危险的。
这对用户来说很少很明显,因为处理以小块快速发生。例如,JavaScript 检测按钮单击、运行计算并更新 DOM。完成后,浏览器可以自由处理队列中的下一项。
使用回调异步
单线程引发问题。当 JavaScript 调用“慢速”进程(例如浏览器中的 Ajax 请求或服务器上的数据库操作)时会发生什么?该操作可能需要几秒钟甚至_几分钟_。浏览器在等待响应时将被锁定。在服务器上,Node.js 应用程序将无法处理进一步的用户请求。
解决方案是异步处理。不是等待完成,而是告诉进程在结果准备就绪时调用另一个函数。这称为回调,它作为参数传递给任何异步函数。
例如:
doSomethingAsync(callback1);
console.log('finished');
function callback1(error) {
if (!error) console.log('doSomethingAsync complete');
}
该函数接受回调作为参数(仅传递对该函数的引用,因此开销很小)。需要多长时间并不重要;我们所知道的是,这将在未来的某个时候执行。控制台将显示以下内容:doSomethingAsync``doSomethingAsync``callback1
finished
doSomethingAsync complete
回调地狱
通常,回调仅由一个异步函数调用。因此,可以使用简洁的匿名内联函数:
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 调用。
承诺
ES2015(ES6)引入了承诺。回调仍然在表面之下使用,但 promise 提供了一种更清晰的语法来_链接_异步命令,以便它们串联运行(下一节将详细介绍)。
若要启用基于承诺的执行,必须更改基于异步回调的函数,以便它们立即返回 promise 对象。该对象_承诺_在将来的某个时候运行两个函数之一(作为参数传递):
resolve
:处理成功完成时运行的回调函数reject
:发生故障时运行的可选回调函数
在下面的示例中,数据库 API 提供了一个接受回调函数的方法。外部函数立即返回一个新的承诺,并在建立连接或失败后运行:connect``asyncDBconnect``resolve``reject
const db = require('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() 实用程序,用于将基于回调的函数转换为基于 promise 的替代方案。有几个条件:
- 回调必须作为最后一个参数传递给异步函数
- 回调函数必须预期错误,后跟值参数
例:
const
util = require('util'),
fs = require('fs'),
readFileAsync = util.promisify(fs.readFile);
readFileAsync('file.txt');
异步链接
任何返回 promise 的东西都可以启动一系列在方法中定义的异步函数调用。每个都传递来自前一个的结果:.then()``resolve
asyncDBconnect('http://localhost:1234')
.then(asyncGetSession)
.then(asyncGetUser)
.then(asyncLogAccess)
.then(result => {
console.log('complete');
return result;
})
.catch(err => {
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(() => {
});
}
充满希望的未来?
承诺减少了回调地狱,但引入了自己的问题。
教程通常没有提到_整个承诺链是异步的_。任何使用一系列 promise 的函数都应该返回自己的 promise 或在 final 或方法中运行回调函数。.then()``.catch()``.finally()
语法通常看起来比回调更复杂,有很多错误,调试可能会有问题。但是,学习基础知识至关重要。
异步/等待
承诺可能令人生畏,因此ES2017引入了.虽然它可能只是语法糖,但它使承诺更加甜蜜,你可以完全避免链条。请考虑以下基于承诺的示例:async``await``.then()
function connect() {
return new Promise((resolve, reject) => {
asyncDBconnect('http://localhost:1234')
.then(asyncGetSession)
.then(asyncGetUser)
.then(asyncLogAccess)
.then(result => resolve(result))
.catch(err => reject(err))
});
}
(() => {
connect();
.then(result => console.log(result))
.catch(err => console.log(err))
})();
要重写它,请使用:async/await
- 外部函数前面必须有一个语句
async
- 调用异步的、基于 promise 的函数之前必须加上 以确保在执行下一个命令之前完成处理
await
async function connect() {
try {
const
connection = await asyncDBconnect('http://localhost:1234'),
session = await asyncGetSession(connection),
user = await asyncGetUser(session),
log = await asyncLogAccess(user);
return log;
}
catch (e) {
console.log('error', err);
return null;
}
}
(async () => { await connect(); })();
await
有效地使每个调用看起来好像是同步的,同时不占用JavaScript的单个处理线程。此外,函数总是返回一个承诺,因此它们反过来可以被其他函数调用。async``async
async/await
代码可能不会更短,但有相当大的好处:
- 语法更简洁。括号更少,出错更少。
- 调试更容易。可以在任何语句上设置断点。
await
- 错误处理更好。 块的使用方式与同步代码相同。
try/catch
- 支持很好。它在所有现代浏览器和 Node 7.6+ 中实现。
也就是说,并非一切都是完美的...
承诺,承诺
async/await
依赖于承诺,而承诺最终依赖于回调。这意味着您仍然需要了解承诺是如何运作的。
此外,当使用多个异步操作时,没有直接等同于 Promise.all 或 Promise.race。很容易忘记 ,这比使用一系列不相关的命令更有效。Promise.all``await
JavaScript 之旅
异步编程是 JavaScript 中无法避免的挑战。回调在大多数应用程序中是必不可少的,但它很容易纠缠于深度嵌套的函数中。
承诺抽象回调,但有许多语法陷阱。转换现有函数可能是一件苦差事,链看起来仍然很混乱。.then()
幸运的是,提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写 JavaScript 的方式,甚至可以让你欣赏承诺——如果你以前没有的话!async/await
转载自:https://juejin.cn/post/7239321198035107899