JavaScript内核与高级编程之:`Promise.any`和`Promise.allSettled`:其在并发控制中的新用途。

各位老铁,大家好!我是你们的老朋友,今天咱不聊风花雪月,来点硬核的——Promise.anyPromise.allSettled,看看这哥俩在并发控制里能整出啥新花样。

开场白:并发控制,你我的痛

话说,咱们写代码,尤其是涉及到网络请求、异步操作的时候,并发控制绝对是个绕不开的坎儿。搞不好,辛辛苦苦写的代码,就成了并发的牺牲品,bug 满天飞,用户体验稀烂。

以前,我们控制并发,要么自己手写各种复杂的逻辑,要么用一些现成的库,比如 async.js,但总觉得差点意思,不够优雅,不够现代。

现在不一样了,ES2020 带来了 Promise.anyPromise.allSettled,这两个家伙,简直是并发控制领域的两员猛将,能让我们轻松搞定各种并发场景。

第一回合:Promise.any——只要你行,我就行!

Promise.any 就像一个“选秀节目”,给它一堆 Promise,只要其中一个成功了,它就成功了!如果所有的 Promise 都失败了,它才会失败,并抛出一个 AggregateError 错误,告诉你“没一个行的!”

语法:

Promise.any(iterable);
  • iterable: 一个可迭代对象,通常是一个 Promise 数组。

例子:

假设我们要从三个不同的服务器获取数据,但只要有一个服务器能成功返回数据,我们就认为成功了。

const fetchFromServer1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟服务器1成功返回数据
      resolve("Data from Server 1");
    }, 500);
  });
};

const fetchFromServer2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟服务器2失败
      reject("Server 2 failed");
    }, 800);
  });
};

const fetchFromServer3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // 模拟服务器3成功返回数据
      resolve("Data from Server 3");
    }, 1200);
  });
};

Promise.any([fetchFromServer1(), fetchFromServer2(), fetchFromServer3()])
  .then(result => {
    console.log("Successfully fetched data:", result); // 输出: Successfully fetched data: Data from Server 1
  })
  .catch(error => {
    console.error("All servers failed:", error);
  });

在这个例子中,fetchFromServer1 最先成功,所以 Promise.any 会立即resolve,并返回 Data from Server 1。即使 fetchFromServer2 失败了,fetchFromServer3 后来也成功了,都不会影响 Promise.any 的结果。

并发控制中的应用:

  1. 竞速请求 (Race Condition): 当我们需要从多个来源获取相同的数据时,可以使用 Promise.any,只要有一个来源返回了数据,就立即使用,而忽略其他的请求。 这可以显著提高响应速度。

    const getDataFromCache = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 模拟从缓存读取数据
          const cachedData = localStorage.getItem("myData");
          if (cachedData) {
            resolve(JSON.parse(cachedData));
          } else {
            reject("No data in cache");
          }
        }, 100);
      });
    };
    
    const getDataFromServer = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 模拟从服务器获取数据
          const data = { name: "John Doe", age: 30 };
          localStorage.setItem("myData", JSON.stringify(data)); // 模拟缓存数据
          resolve(data);
        }, 500);
      });
    };
    
    Promise.any([getDataFromCache(), getDataFromServer()])
      .then(data => {
        console.log("Data:", data); // 先从缓存获取数据,如果没有,再从服务器获取
      })
      .catch(error => {
        console.error("Failed to get data:", error);
      });
  2. 故障转移 (Failover): 当我们需要从多个服务器中选择一个可用的服务器时,可以使用 Promise.any。如果一个服务器宕机了,Promise.any 会自动尝试其他的服务器,直到找到一个可用的服务器。

    const serverUrls = ["https://server1.example.com", "https://server2.example.com", "https://server3.example.com"];
    
    const fetchDataFromServer = url => {
      return fetch(url)
        .then(response => {
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }
          return response.json();
        })
        .catch(error => {
          console.warn(`Failed to fetch from ${url}: ${error}`);
          return Promise.reject(error); // 关键: 必须 reject,才能让 Promise.any 尝试下一个 Promise
        });
    };
    
    const fetchFromAnyServer = () => {
      const promises = serverUrls.map(url => fetchDataFromServer(url));
      return Promise.any(promises);
    };
    
    fetchFromAnyServer()
      .then(data => {
        console.log("Data from available server:", data);
      })
      .catch(error => {
        console.error("All servers failed:", error);
      });

    注意:fetchDataFromServer 中,如果 fetch 失败了,必须 reject 这个 Promise,才能让 Promise.any 继续尝试其他的 Promise。 如果只是 console.warn 然后 resolve 一个 undefinedPromise.any 会认为这个 Promise 成功了,从而导致错误的结果。

第二回合:Promise.allSettled——一个都不能少!

Promise.allSettled 就像一个“尽职尽责的监工”,给它一堆 Promise,它会等待所有的 Promise 都完成(不管是成功还是失败),然后返回一个结果数组,告诉你每个 Promise 的状态。

语法:

Promise.allSettled(iterable);
  • iterable: 一个可迭代对象,通常是一个 Promise 数组。

例子:

假设我们要同时执行三个任务,但我们需要知道每个任务的执行结果,不管是成功还是失败。

const task1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Task 1 completed");
    }, 300);
  });
};

const task2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("Task 2 failed");
    }, 500);
  });
};

const task3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Task 3 completed");
    }, 700);
  });
};

Promise.allSettled([task1(), task2(), task3()])
  .then(results => {
    console.log("All tasks completed:", results);
    // 输出:
    // All tasks completed: [
    //   { status: 'fulfilled', value: 'Task 1 completed' },
    //   { status: 'rejected', reason: 'Task 2 failed' },
    //   { status: 'fulfilled', value: 'Task 3 completed' }
    // ]
  });

在这个例子中,Promise.allSettled 会等待 task1task2task3 都完成,然后返回一个数组,数组中的每个元素都是一个对象,包含 status (fulfilled 或 rejected) 和 value (如果成功) 或 reason (如果失败)。

并发控制中的应用:

  1. 日志记录 (Logging): 当我们需要记录多个操作的执行结果时,可以使用 Promise.allSettled。 即使某个操作失败了,我们仍然可以记录下失败的原因,方便排查问题。

    const logActivity = (activity, success) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (success) {
            console.log(`Logged: ${activity} - Success`);
            resolve(`Logged: ${activity} - Success`);
          } else {
            console.error(`Logged: ${activity} - Failed`);
            reject(`Logged: ${activity} - Failed`);
          }
        }, 200);
      });
    };
    
    const performTask = (taskName, shouldSucceed) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (shouldSucceed) {
            console.log(`${taskName} completed successfully`);
            resolve(`${taskName} completed successfully`);
          } else {
            console.error(`${taskName} failed`);
            reject(`${taskName} failed`);
          }
        }, 500);
    });
    };
    
    const tasks = [
      performTask("Task A", true).then(() => logActivity("Task A", true)).catch(() => logActivity("Task A", false)),
      performTask("Task B", false).then(() => logActivity("Task B", true)).catch(() => logActivity("Task B", false)),
      performTask("Task C", true).then(() => logActivity("Task C", true)).catch(() => logActivity("Task C", false))
    ];
    
    Promise.allSettled(tasks)
      .then(results => {
        console.log("All tasks logged:", results);
      });

    在这个例子中,即使 performTask("Task B", false) 失败了,我们仍然可以记录下失败的信息。

  2. 数据同步 (Data Synchronization): 当我们需要将数据同步到多个服务器时,可以使用 Promise.allSettled。 我们可以知道哪些服务器同步成功了,哪些服务器同步失败了,然后根据情况进行重试或者其他处理。

    const syncDataToServer = (serverId, data) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 模拟数据同步
          const success = Math.random() > 0.2; // 模拟 80% 的成功率
          if (success) {
            console.log(`Data synchronized to server ${serverId}`);
            resolve(`Data synchronized to server ${serverId}`);
          } else {
            console.error(`Failed to synchronize data to server ${serverId}`);
            reject(`Failed to synchronize data to server ${serverId}`);
          }
        }, 400);
      });
    };
    
    const serverIds = ["Server A", "Server B", "Server C"];
    const data = { message: "Hello, world!" };
    
    const syncPromises = serverIds.map(serverId => syncDataToServer(serverId, data));
    
    Promise.allSettled(syncPromises)
      .then(results => {
        console.log("Data synchronization results:", results);
    
        const failedServers = results
          .filter(result => result.status === "rejected")
          .map(result => result.reason);
    
        if (failedServers.length > 0) {
          console.warn("Failed to synchronize data to the following servers:", failedServers);
          // 可以根据情况进行重试或者其他处理
        }
      });

    在这个例子中,我们可以知道哪些服务器同步成功了,哪些服务器同步失败了,然后可以根据失败的服务器列表进行重试或其他处理。

第三回合:Promise.any vs Promise.allSettled——谁更胜一筹?

特性 Promise.any Promise.allSettled
成功条件 只要有一个 Promise 成功,就立即成功。 必须等待所有的 Promise 都完成(不管是成功还是失败)。
失败条件 所有的 Promise 都失败,才会失败。 永远不会失败,总是会返回一个结果数组。
返回值 返回第一个成功的 Promise 的值。 返回一个数组,数组中的每个元素都是一个对象,包含 status (fulfilled 或 rejected) 和 value (如果成功) 或 reason (如果失败)。
使用场景 竞速请求、故障转移等,只需要一个 Promise 成功即可的场景。 日志记录、数据同步等,需要知道所有 Promise 的执行结果的场景。
并发控制关注点 注重速度,尽快得到一个可用的结果。 注重完整性,确保所有的操作都完成了,并记录下结果。

总结:

Promise.anyPromise.allSettled 是 ES2020 带来的两个强大的并发控制工具。Promise.any 适用于只需要一个 Promise 成功的场景,例如竞速请求和故障转移;Promise.allSettled 适用于需要知道所有 Promise 的执行结果的场景,例如日志记录和数据同步。

掌握了这两个工具,你的并发控制能力将会大大提升,写出更健壮、更高效的代码。

终极奥义:组合拳

实际上,Promise.anyPromise.allSettled 还可以组合使用,创造出更强大的并发控制模式。 比如,先使用 Promise.allSettled 获取所有任务的结果,然后根据结果决定是否使用 Promise.any 重新尝试失败的任务。

这就像是,你先派出所有的侦察兵去侦查,然后根据侦察结果,决定是否需要派出特种部队进行突击。

const attemptTask = (taskName, attemptNumber) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() > 0.3; // 模拟 70% 的成功率
      if (success) {
        console.log(`${taskName} - Attempt ${attemptNumber} completed successfully`);
        resolve(`${taskName} - Attempt ${attemptNumber} completed successfully`);
      } else {
        console.error(`${taskName} - Attempt ${attemptNumber} failed`);
        reject(`${taskName} - Attempt ${attemptNumber} failed`);
      }
    }, 300);
  });
};

const tasks = [
  attemptTask("Task X", 1),
  attemptTask("Task Y", 1),
  attemptTask("Task Z", 1)
];

Promise.allSettled(tasks)
  .then(results => {
    console.log("Initial task results:", results);

    const failedTasks = results
      .filter(result => result.status === "rejected")
      .map((result, index) => ({ taskName: tasks[index].toString().match(/attemptTask("(.+)",/)[1], index })); //extract task name from the function string

    if (failedTasks.length > 0) {
      console.warn("Retrying failed tasks:", failedTasks.map(t => t.taskName));

      const retryPromises = failedTasks.map(task => attemptTask(task.taskName, 2)); // retry once

      return Promise.any(retryPromises); // Retry only the failed tasks
    } else {
      console.log("All tasks completed successfully on the first attempt.");
      return Promise.resolve(); // All tasks succeeded, so resolve immediately
    }
  })
  .then(retryResult => {
    if (retryResult) {
      console.log("At least one retry succeeded:", retryResult);
    } else {
      console.warn("All retries failed.");
    }
  })
  .catch(error => {
    console.error("An error occurred:", error);
  });

在这个例子中,我们先使用 Promise.allSettled 执行所有的任务,然后找出失败的任务,并使用 Promise.any 重新尝试这些失败的任务。 如果所有的任务都第一次就成功了,则不需要重试。

总结的总结:

Promise.anyPromise.allSettled 是并发控制的利器,但它们并不是万能的。 在使用它们的时候,需要根据具体的场景进行选择,并注意一些细节问题,才能发挥它们最大的威力。

希望今天的分享对大家有所帮助! 记住,代码的世界,充满挑战,也充满乐趣! 祝大家编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注