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
回调地狱
通常,回调只能由一个异步函数调用。 因此,可以使用简洁的匿名内联函数:
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)引入了Promises 。 回调仍然在表层以下使用,但是Promises提供了一种更清晰的语法,该语法将异步命令链接在一起,因此它们可以串行运行(在下一节中有更多关于)。
要启用基于Promise的执行,必须更改基于异步回调的函数,以便它们立即返回Promise对象。 该对象承诺将来会在某些时候运行以下两个函数之一(作为参数传递):
-
resolve
:处理成功完成时运行的回调函数,以及 -
reject
:发生故障时运行的可选回调函数。
在下面的示例中,数据库API提供了一个connect()
方法,该方法接受回调函数。 建立连接或失败后,外部asyncDBconnect()
函数会立即返回一个新的Promise并运行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()实用程序 ,可将基于回调的函数转换为基于Promise的替代方案。 有两个条件:
- 回调必须作为最后一个参数传递给异步函数,并且
- 回调函数必须期望一个错误,后跟一个value参数。
例:
// Node.js: promisify fs.readFile
const
util = require('util'),
fs = require('fs'),
readFileAsync = util.promisify(fs.readFile);
readFileAsync('file.txt');
各种客户端库也提供promisify选项,但您可以自己创建几行:
// promisify a callback function passed as the last parameter
// the callback function must accept (err, data) parameters
function promisify(fn) {
return function() {
return new Promise(
(resolve, reject) => fn(
...Array.from(arguments),
(err, data) => err ? reject(err) : resolve(data)
)
);
}
}
// example
function wait(time, callback) {
setTimeout(() => { callback(null, 'done'); }, time);
}
const asyncWait = promisify(wait);
ayscWait(1000);
异步链接
任何返回Promise的内容都可以启动.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()
方法,无论结果如何,该方法都可以运行任何最终逻辑,例如清理,关闭数据库连接等。目前仅Chrome和Firefox支持,但技术委员会39已发布.finally ()polyfill 。
function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
// tidy-up here!
});
}
具有Promise.all()的多个异步调用
Promise .then()
方法一个接一个地运行异步函数。 如果顺序无关紧要(例如,初始化不相关的组件),则可以更快地同时启动所有异步函数,并在最后一个(最慢的)函数运行resolve
。
这可以通过Promise.all()
实现。 它接受一个函数数组并返回另一个Promise。 例如:
Promise.all([ async1, async2, async3 ])
.then(values => { // array of resolved values
console.log(values); // (in same order as function array)
return values;
})
.catch(err => { // called on any reject
console.log('error', err);
});
如果任一异步函数调用reject
Promise.all()
立即终止。
具有Promise.race()的多个异步调用
Promise.race()
与Promise.race()
类似,除了Promise.all()
在第一个 Promise解析或拒绝后会立即解决或拒绝。 只有最快的基于Promise的异步功能才能完成:
Promise.race([ async1, async2, async3 ])
.then(value => { // single value
console.log(value);
return value;
})
.catch(err => { // called on any reject
console.log('error', err);
});
有前途的未来?
承诺减少了回调地狱,但引入了自己的问题。
教程常常没有提到整个Promise链是异步的 。 使用一系列承诺的最终要么返回自己的承诺或运行回调函数的任何功能.then()
.catch()
或.finally()
方法。
我也有一个表白: 许诺使我困惑了很长时间 。 语法似乎通常比回调复杂,有很多地方会出错,并且调试可能会出现问题。 但是,必须学习基础知识。
更多承诺资源:
异步/等待
承诺可能令人生畏,因此ES2017引入了async
和await
。 尽管它可能只是语法糖,但它使Promises更加甜美,并且您可以完全避免.then()
链。 考虑下面的基于Promise的示例:
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))
});
}
// run connect (self-executing function)
(() => {
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;
}
}
// run connect (self-executing async function)
(async () => { await connect(); })();
await
有效地使每个调用看起来像是同步的,而不会占用JavaScript的单个处理线程。 另外, async
函数总是返回Promise,因此它们可以被其他async
函数调用。
async
/ await
代码可能不会更短,但是有很多好处:
- 语法更简洁。 括号更少,出错更少。
- 调试更容易。 可以在任何
await
语句上设置断点。 - 错误处理更好。
try
/catch
块可以与同步代码相同的方式使用。 - 支持很好。 它已在所有浏览器(IE和Opera Mini除外)和Node 7.6+中实现。
就是说,并非所有事物都是完美的……
承诺,承诺
async
/ await
仍然依赖于Promises,而Promises最终依赖于回调。 您需要了解Promises的工作原理,并且没有Promise.all()
和Promise.race()
直接等效项。 容易忘记Promise.all()
,它比使用一系列不相关的await
命令更有效。
异步循环中的异步等待
在某些时候,您将尝试在同步循环内调用异步函数。 例如:
async function process(array) {
for (let i of array) {
await doSomething(i);
}
}
它不会工作。 这也不会:
async function process(array) {
array.forEach(async i => {
await doSomething(i);
});
}
循环本身保持同步,并且将始终在内部异步操作之前完成。
ES2018引入了异步迭代器,与常规迭代器类似,不同之处在于next()
方法返回Promise。 因此, await
关键字可以与for … of
循环一起使用,以串行运行异步操作。 例如:
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
但是,在实现异步迭代器之前,最好将数组项map
到async
函数并使用Promise.all()
运行它们。 例如:
const
todo = ['a', 'b', 'c'],
alltodo = todo.map(async (v, i) => {
console.log('iteration', i);
await processSomething(v);
});
await Promise.all(alltodo);
这具有并行运行任务的好处,但是不可能将一次迭代的结果传递给另一次迭代,并且映射大型数组可能在计算上很昂贵。
尝试/抓住丑陋
如果您省略try
/ catch
失败的await
, async
功能将自动退出。 如果您有一长串异步await
命令,则可能需要多个try
/ catch
块。
一种选择是高阶函数,该函数捕获错误,因此try
/ catch
块变得不必要(感谢@wesbos的建议):
async function connect() {
const
connection = await asyncDBconnect('http://localhost:1234'),
session = await asyncGetSession(connection),
user = await asyncGetUser(session),
log = await asyncLogAccess(user);
return true;
}
// higher-order function to catch errors
function catchErrors(fn) {
return function (...args) {
return fn(...args).catch(err => {
console.log('ERROR', err);
});
}
}
(async () => {
await catchErrors(connect)();
})();
但是,在应用程序必须以与其他错误不同的方式对某些错误做出反应的情况下,此选项可能不切实际。
尽管存在一些陷阱,但async
/ await
是JavaScript的优雅补充。 更多资源:
JavaScript之旅
异步编程是JavaScript中无法避免的挑战。 回调在大多数应用程序中都是必不可少的,但是很容易陷入深度嵌套的函数中。
承诺抽象回调,但是有许多语法陷阱。 转换现有函数可能很麻烦, .then()
链看起来仍然很杂乱。
幸运的是, async
/ await
提供了清晰度。 代码看起来是同步的,但是它不能独占单个处理线程。 它将改变您编写JavaScript的方式,甚至可能使您欣赏Promises(如果您以前从未这样做过!)。
From: https://www.sitepoint.com/flow-control-callbacks-promises-async-await/