正确的将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 userconst user = await `[user object ${id}]`;// get avatar imageconst image = await `[image blob for ${user}]`;// resize imageconst resizedImage = await `[${image} resized to width:${width}]`;// send the resized imageres(resizedImage);

代码更短,看起来一点也不异步。

但是现在每个变量都在整个函数的范围内。 如果一个步骤需要存储某些内容,则没有什么可以阻止下一步访问它。 同样,可以访问上一步的每个结果:

// get avatar imageconst imageRequest = ...;const image = await `[image blob for ${user}]`;// resize image// imageRequest is in scope// user is also in scopeconst 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 userconst user = await (async () => {    return await `[user object ${id}]`;})();// get avatar imageconst image = await (async () => {    return await `[image blob for ${user}]`;})();// resize imageconst resizedImage = await (async () => {    return await `[${image} resized to width:${width}]`;})();// send the resized imageres(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

关注我不迷路