正确的将Promise链重构为async函数
原文地址:advancedweb.hu/how-to-refactor-a-promise-chain-to-async-functions/#refactoring-to-asyncawait原文作者:Tamás Sallai
0.引言
将一系列 then() 函数转换为async/await,而不会丢失函数作用域
1. 重构成async/await
现在,async/await 已经得到了广泛的支持、应用 ,Promises 已经是一种老的解决方案了,但是它们仍然是驱动所有异步操作的引擎。 但是构造一个并使用.then()
函数进行异步链式操作的情况越来越少。 这提醒我们从基于Promise的链重构为 async/await。
例如,这个异步代码使用.then()
进行链式操作:
doSomething()
.then(doSomethingElse)
.then(finishWithSomething);
使用 async/await 进行重构:
const sth = await doSomething();
const sthElse = await doSomethingElse(sth);
const fin = await finishWithSomething(sthElse);
重构的代码不仅简短且更加容易识别,因为它看起来像是一个同步代码。 所有复杂性高都可以由async / await构造处理。
但是,当我将基于.then()
结构转换为 async / await 时,我总是有一种感觉,即原始代码在确定步骤中的变量方面做的更好,而async / await版本则泄漏了它们。
2. Promise链
让我们看一个假设的多步骤处理过程,该过程获取并调整用户ID的头像:
const width = 200;
const res = (result) => console.log(`Sending ${result}`);
Promise.resolve(15)
.then((id) => {
// get user
return `[user object ${id}]`;
}).then((user) => {
// get avatar image
return `[image blob for ${user}]`;
}).then((image) => {
// resize image
return `[${image} resized to width:${width}]`;
}).then((resizedImage) => {
// send the resized image
res(resizedImage);
});
// Sending [[image blob for [user object 15]] resized to width:200]
每个步骤都可以使用异步操作,例如连接到数据库或使用远程API。
通过使用.then()
,在函数内部声明的变量是该函数的局部变量,无法从其他步骤访问。 由于范围内变量的数量受到限制,因此使代码更易于理解。
例如,如果使用 fetch 获取图像并将结果存储在一个值中,则在下一步中将看不到它:
...
.then((user) => {
const imageRequest = ...;
// ...
}).then((image) => {
// imageRequest is not accessible
})
对于参数也是如此:
...
.then((user) => {
// ...
}).then((image) => {
// user is not accessible
})
3. 转换成 async/await
将上面的代码转换为 async / await 很简单,只需在步骤之前添加 awaits 并将结果分配给变量:
const width = 200;
const res = (result) => console.log(`Sending ${result}`);
const id = 15;
// get user
const user = await `[user object ${id}]`;
// get avatar image
const image = await `[image blob for ${user}]`;
// resize image
const resizedImage = await `[${image} resized to width:${width}]`;
// send the resized image
res(resizedImage);
代码更短,看起来一点也不异步。
但是现在每个变量都在整个函数的范围内。 如果一个步骤需要存储某些内容,则没有什么可以阻止下一步访问它。 同样,可以访问上一步的每个结果:
// get avatar image
const imageRequest = ...;
const image = await `[image blob for ${user}]`;
// resize image
// imageRequest is in scope
// user is also in scope
const resizedImage = await `[${image} resized to width:${width}]`;
转换后的代码相比之前代码,没有了异步操作的边界性和清晰变量定义的特征。
4. Async IIFEs
由于较大的问题之一是可变作用域,因此可以通过为每个步骤重新引入一个函数来解决。 这样可以防止在内部声明的变量泄漏到下一步。
一个async IIFE结构:
const result = await (async () => {
// ...
})();
上面使用这种方法的示例如下所示:
const width = 200;
const res = (result) => console.log(`Sending ${result}`);
const id = 15;
// get user
const user = await (async () => {
return await `[user object ${id}]`;
})();
// get avatar image
const image = await (async () => {
return await `[image blob for ${user}]`;
})();
// resize image
const resizedImage = await (async () => {
return await `[${image} resized to width:${width}]`;
})();
// send the resized image
res(resizedImage);
这可以进行可变作用域,但是前面的结果仍然可用。
但是这种方法最大的问题是它看起来很难看。虽然是同步代码形式,但看起来根本不熟悉。
5. 异步递归
另一种方法是使用类似于功能收集管道的结构。 它需要为每个步骤使用单独的函数,然后进行异步化简,按顺序调用每个函数。 这种结构不仅使所有功能分离,因为每个函数只能访问其参数,而且还可以促进代码重用。
5.1 处理步骤
第一步是将每一个中间步骤移到单独的异步函数上:
const getUser = async (id) => {
// get user
return `[user object ${id}]`;
};
const getImage = async (user) => {
// get avatar image
return `[image blob for ${user}]`;
};
// see below
// const resizeImage = ...
const sendImage = async (image) => {
// send the resized image
console.log(`Sending ${image}`);
};
当一个函数不仅需要先前的结果而且还需要一些其他参数时,请使用一个高阶函数,该函数首先获取这些额外的参数,然后再获取先前的结果:
const resizeImage = (width) => async (image) => {
// resize image
return `[${image} resized to width:${width}]`;
};
请注意,上述所有函数均符合 async(prevResult)=> ... nextResult
或(parameters)=> async(prevResult)=> ... nextResult
的结构。 通过使用参数调用后者,可以将后者转换为前者。
5.2 异步递归结构
通过获得前一个结果并与下一个产生Promise的函数,reduce可以调用它们,同时还可以处理等待的结果:
[
getUser,
getImage,
resizeImage(200),
sendImage,
].reduce(async (memo, fn) => fn(await memo), 15);
在此示例中,函数定义了步骤,值流经它们。 15是初始值(前面示例中的userId),reduce的结果是最后一个函数的结果。
这种结构保留了基于Promise链的原始实现中明确定义的步骤,同时还利用了异步功能。
6. 结论
不赞成使用Promises,因为使用async / await代替它们会产生易于理解的代码。 但是,通过用 awaits 替换thens
来重写所有使用 Promises 的东西,通常会产生一个长期难以维护的结构。 使用异步缩减有助于保留原始结构。
推荐阅读
- 如何在 Array.forEach 中正确使用 async
- 如何在 Array.filter 中正确使用 async
- 如何在 Array.reduce 中正确使用 async
- 如何在 Array.map 中正确使用 async
- 如何在 Array.some 中正确使用 async