JavaScript - Promise.allSettled + Array.reduce()

JavaScript - Promise.allSettled + Array.reduce()

简介

想象一下这种获取用户语言的方法:

const getUserLanguage = (userId) => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);


(async () => {
    try {
        const language = await getUserLanguage("Mike")
        console.log(`Language: ${language}`);
    } catch(err) {
        console.error(err);
    }
})();

现在,我正在尝试对多个用户的语言进行分组,执行并行请求:

const getUserLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const promiseResults = await Promise.allSettled(
        userIds.reduce(async (acc, userId) => {
            const language = await getUserLanguage(userId);

            (acc[language] = acc[language] ?? []).push(userId);

            return acc;
        }, {})
    );
  
    console.log({ promiseResults });
  
    // Filter fulfilled promises
    const result = promiseResults
        .filter(({ status }) => status === "fulfilled")
        .map(({ value }) => value);
  
    return result;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

问题

但是我的实现不起作用:

const promiseResults = await Promise.allSettled(
    userIds.reduce(async (acc, userId) => {
        const language = await getUserLanguage(userId);

        (acc[language] = acc[language] ?? []).push(userId);

        return acc;
    }, {})
);

我怎样才能得到像

这样的输出
{
    "es": ["Mike", "Saul"],
    "en": ["Walter"],
}

使用 Promise.allSettled 结合 .reduce?

您的 .reduce 正在构建一个对象,其中每个值都是一个 Promise。这样的对象不是 .allSettled 可以理解的东西 - 你必须向它传递一个数组。

我会在外部创建一个对象,它会在 .map 回调中发生变化。这样,您将拥有一个 .allSettled 可以使用的 Promises 数组,并且还具有所需形状的对象。

const getLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const grouped = {};
    await Promise.allSettled(
        userIds.map(async (userId) => {
            const language = await getLanguage(userId);
            (grouped[language] = grouped[language] ?? []).push(userId);
        })
    );
    return grouped;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

一个不依赖于 .map 中的 side-effects 的选项是 return 地图回调中的 userId 和语言,然后过滤 allSettled 结果只包括好的结果,然后 把它变成一个对象。

const getLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const settledResults = await Promise.allSettled(
        userIds.map(async (userId) => {
            const language = await getLanguage(userId);
            return [userId, language];
        })
    );
    const grouped = {};
    settledResults
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value)
        .forEach(([userId, language]) => {
            (grouped[language] = grouped[language] ?? []).push(userId);
        });
    return grouped;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

为此,我会使用两个实用函数编写一个主函数:一个根据函数的结果对一组元素进行分组,另一个采用谓词函数并将数组划分为它所针对的那些returns true 和它 returns false 的那些。这两个依次使用 push 效用函数,该函数将 Array.prototype.push 简单地具体化为一个普通函数。

main 函数将 getUserLanguage 函数映射到用户上,调用 Promise.allSettled 结果,然后我们映射生成的承诺,将原始 userId 与承诺结果。 (如果伪造的 getUserLanguage 返回了一个同时具有 userIdlanguage 属性的对象,那么这一步就没有必要了。)然后我们划分结果承诺以分离出 fulfilled 来自 rejected 个。我这样做是因为您的问题没有说明如何处理被拒绝的语言查找。我选择在输出中再添加一个条目。这里除了 esen,我们还在 _errors 下得到了 userId 的列表。如果我们想忽略这些,那么我们可以用 filter 替换 partition 并简化最后一步。最后一步采用成功的结果和失败的结果,将成功的结果与我们的 group 助手组合成一个对象,并通过将失败映射到它们的 userId 来附加 _errors

它可能看起来像这样:

// dummy implementation, resolving to random language, or rejecting with error
const getUserLanguage = (userId) => new Promise ((resolve, reject) => {if (Math.random() < 0.3) resolve("en"); if (Math.random() < 0.6) resolve("es"); reject("Unexpected error.");});
 

// utility functions
const push = (x) => (xs) => 
  (xs .push (x), xs)
const partition = (fn) => (xs) =>
  xs .reduce (([y, n], x) => fn (x) ? [push (x) (y), n] : [y, push (x) (n)], [[], []])
const group = (getKey, getValue) => (xs) => 
  xs .reduce ((a, x, _, __, key = getKey (x)) => ((a [key] = push (getValue (x)) (a[key] ?? [])), a), {})


// main function
const groupUsersByLanguage = (users) => Promise .allSettled (users .map (getUserLanguage))
  .then (ps => ps .map ((p, i) => ({...p, user: users [i]})))
  .then (partition (p => p .status == 'fulfilled'))
  .then (([fulfilled, rejected]) => ({
    ...group (x => x .value, x => x.user) (fulfilled),
    _errors: rejected .map (r => r .user)
  }))


// sample data
const users = ['fred', 'wilma', 'betty', 'barney', 'pebbles', 'bambam', 'yogi', 'booboo']


// demo
groupUsersByLanguage (users)
  .then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}

这会产生这样的输出(YMMV 因为 random 调用):

{
  en: [
    "fred",
    "wilma",
    "barney"
  ],
  es: [
    "bambam",
    "yogi",
    "booboo"
  ],
  _errors: [
    "betty",
    "pebbles"
  ]
}

请注意,这些效用函数是 general-purpose。如果我们手头有自己的此类工具库,我们可以毫不费力地编写这样的函数。

这样做的另一种选择是首先使用以下方法获取所有语言:

const languages = await Promise.allSettled(userIds.map(getLanguage));

然后与 userIds 一起压缩并进一步处理。

async function getLanguage() {
  if (Math.random() < 0.3) return "en";
  if (Math.random() < 0.6) return "es";
  throw "Unexpected error.";
}

function zip(...arrays) {
  if (!arrays[0]) return;
  return arrays[0].map((_, i) => arrays.map(array => array[i]));
}

async function groupUsersByLanguage(userIds) {
  const languages = await Promise.allSettled(userIds.map(getLanguage));
    
  const groups = {};
  for (const [userId, language] of zip(userIds, languages)) {
    if (language.status != "fulfilled") continue;
    
    groups[language.value] ||= [];
    groups[language.value].push(userId);
  }
  
  return groups;
}

(async () => {
  const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
  const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
  console.log(usersGroupedByLanguage);
})();

如果您对创建 zip() 助手不感兴趣,您可以使用“普通”for-loop:

const groups = {};
for (let i = 0; i < userIds.length; i += 1) {
  if (languages[i].status != "fulfilled") continue;
  
  groups[languages[i].value] ||= [];
  groups[languages[i].value].push(userId);
}