使用 async/await 是必须避免的陷阱
如果我们使用过 nodejs,那么我们可能已经在 javaSoript 中使用了异步操作。异步任务是一个独立于 JavaSoript 引擎的主线程执行的操作。从本质上讲,这就是应用程序功能没有阻塞的 UI 的原因。
nodejs 的单线程性质,这一点极其重要。
Node.js 利用事件循环来处理所有异步操作,保留了用于计算函数的主线程。
假设我们对事件循环有相当的了解。在这种情况下,我们会明白,当在调用堆栈中发现一个非同步操作时,JS会把它放到线程池上,线程池将通过 libuv 库异步地执行它。之后 libuv 将执行操作并将其推进到"事件队列"中。"事件队列将被持续监控,事件队列中的事件将被提取并在处理异步操作响应的回调函数上执行。这基本上就是 nodejs 如何处理异步操作。
例如我们可以使用 JavaSrispt 中 Promise 建立异步操作。Promise 返回的一个对象,代表其进程。
// 返回一个Promise对象
function fetchData() {
return new Promise((resolve, reject) => {
// 使用setTimeout 模拟一个异步操作
setTimeout(() => {
const data = 'Sample Data';
const success = true;
if (success) {
resolve(data);
} else {
reject('Error: Unable to fetch data');
}
}, 2000);
});
}
const fetchDataPromise = fetchData();
fetchDataPromise.then(data => {
console.log('Data received:', data);
})
.catch(error => {
console.error(error);
});
fetchData 方法返回的 Promise 对象包含两种方法:then 和 catch。开发者可以在这两个方法中获取结果。
这使 JavaSrhpt 更加强大,使我们能够构建实时聊天应用程序和API等应用程序。然而,在设计应用程序时,使用JavaSrispt异步操作会有一些常见的缺陷,我们必须考虑这些缺陷,以便能够实现缓解这些问题的方法。
注意:这些陷阱存在于任何 javascript 框架。
回调地狱
使用基于 Promise 的异步操作的关键问题之一是回调地狱。在这种情况下,回调会不断调用 Promise,导致回调链。例如:
function performAsyncOperation(delay: number, message: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve(message);
}, delay);
});
}
export async function callbacks() {
const delay = 1000;
const message = 'Hello World';
return performAsyncOperation(delay, message).then((value) => {
performAsyncOperation(delay, value).then((secondValue) => {
performAsyncOperation(delay, secondValue).then((thirdValue) => {
performAsyncOperation(delay, thirdValue).then(() => {
console.log('End The Callback');
}).catch(() => {
console.log('Error');
});;
}).catch(() => {
console.log('Error');
});;
}).catch(() => {
console.log('Error');
});
});
}
callbacks() 方法 返回一个 performAsyncOperation 并继续添加更多的异步操作。虽然能在生产中发挥完美的作用。但是,当我们考虑到可维护性时,它将是一个混乱的问题。例如,很难看到什么样的回调应用在什么级别。
所以,我们如何避免这种情况?
为了修复回调地狱问题,我们可以将此转换为 async/await 。所以, 我们看看这个的更新代码 :
function performAsyncOperation(delay: number, message: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve(message);
}, delay);
});
}
export async function asyncAwait() {
try {
const delay = 1000;
let message = 'Hello World';
message = await performAsyncOperation(delay, message);
message = await performAsyncOperation(delay, message);
message = await performAsyncOperation(delay, message);
await performAsyncOperation(delay, message);
console.log('End The Callback');
} catch (error) {
console.log('Error:', error);
}
}
我们已经成功地将回调地狱重构为更清洁的方法,它使用async/await,这允许我们执行相同的异步代码,而我们在早些时候执行了一个更干净的方法。await 意味着每一行代码在收到回复之前等候。如果它返回一个成功的响应,它将继续到下一个。但是如果它遇到错误,它将跳到公共的catch 整块。这样做可以避免维护多个错误处理程序和使用单个错误处理程序的需要。
同步函数链
我们已经重构我们的代码,使用async/await 块来处理多个异步调用。但是现在,我们可能会注意到这里有一个新问题:
function performAsyncOperation(delay: number, message: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve(message);
}, delay);
});
}
export async function issueAsyncAwait() {
try {
const delay = 1000;
let message = 'Hello World';
await performAsyncOperation(delay, message);
console.log('Phase 01');
await performAsyncOperation(delay, message);
console.log('End The Callback');
} catch (error) {
console.log('Error:', error);
}
}
在这种情况下,我们想执行 console.log(‘Phase 1’) ,但是performAsyncOperation 方法在一个单独的进程中执行,我们的打印应该是在performAsyncOperation 方法执行前完成对吗?
经过检查,我们可以看到这并不是我们所期待的。怎么回事?
顾名思义,它"等待"整个代码块,直到异步操作返回响应。因此,这使得我们的代码"同步",并创建了一个瀑布调用模式,在这里我们的代码将一个接一个地调用。
因此,如果我们的事件并不相互依赖,如果我们的事件不依赖于非同步操作的输出,我们不必一定要等到非同步操作完成,对吗?
所以,在这种情况下, 考虑使用回调 :
function performAsyncOperation(delay: number, message: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
console.log(message);
resolve(message);
}, delay);
});
}
export async function asyncAwaitFix() {
try {
const delay = 1000;
let message = 'Hello World';
performAsyncOperation(delay, message).then((resp) => console.log(`Process the resp: ${resp}.`));
console.log('Phase 01');
await performAsyncOperation(delay, message);
console.log('End The Callback');
} catch (error) {
console.log('Error:', error);
}
}
如你所见,我们重构了performAsyncOperation 方法并使用 .then() 回调。这样做可以让回调作为一个真正的回调执行,并且不会在代码中创建任何"等待"。为了验证我们的理论,让我们检查一下输出:
如你所见,Phase 01 首先打印了,不再等待到 async 操作完成。
但是要小心使用这个,因为我们可能会创建回调地狱!
循环的性能问题
接下来,让我们谈谈循环。我们都用 JavaScript 写过循环:
for (let i = 0; i < 5; i++) {
console.log('Iteration number:', i);
}
我们循环了一组元素,并对其进行了一些计算。但是如果我们必须在这里执行异步操作呢?假设我们得到了一堆用户身份证。并被要求获取所有身份证的信息(注意:我们的API不支持批量)。 我们可能会这样写:
function getUserInfo(id: number) {
return new Promise((resolve) => {
// 模拟异步
setTimeout(() => {
resolve({ userId: id, name: `User_${id}`, email: `user${id}@example.com` });
}, 1000);
});
}
export async function asyncForLoopIssue(userIds: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
const usersInfo: any[] = [];
for (let i = 0; i < userIds.length; i++) {
const userInfo = await getUserInfo(userIds[i]);
usersInfo.push(userInfo);
}
console.log({ usersInfo });
return usersInfo;
}
现在,这个代码再次没有问题。它将按预期在生产中发挥作用。但是,我们被限制在这里的同步循环。这意味着一旦收集到单个用户信息,我们的循环的下一次迭代将开始。因此,这个函数将在10s后执行,并像这样的同步输出:
这是一个接一个发生的。
但我们该怎么解决?
可以用非线性来执行这个循环:
function getUserInfo(id: number) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ userId: id, name: `User_${id}`, email: `user${id}@example.com` });
}, 1000);
});
}
export async function loopAsyncFix(userIds: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
const promises = userIds.map(async (id) => {
const userInfo = await getUserInfo(id);
return userInfo;
})
const usersInfo = await Promise.all(promises);
console.log({ usersInfo });
return usersInfo;
}
现在,这种方法将产生相同的响应。然而,它的实现方式有点不同。
在方法01中,每次迭代都在当前的async操作完成后开始。
async 意味着它应该在不干扰主线程的情况下执行。
第二种方法坚持真正的异步方法,因为它返回最终将执行的 Promise 对象。因此,虽然我们是顺序运行它,但是它将返回随机调用,每个调用都是独立的,并且在没有相关顺序的情况下按自己的速度执行。