Promise.all跟Promise.race区别

607 阅读9分钟

Promise.all跟Promise.race区别

  • 浏览器对并发请求数量有限制,不同浏览器对同一域名的并发请求数有一定上限(通常是 6 个并发请求左右)。
  • 当一次性上传多个文件时,浏览器可能会阻塞一些请求,从而导致漏传。服务器端对上传文件大小、并发数、超时时间等也可能有限制。如果服务器限制单次请求的文件数量或总大小,超出限制的文件可能无法上传成功。

1. 浏览器限制并发请求数

浏览器通常对并发请求的数量有限制(一般是 6 到 10 个并发请求),如果一次性上传大量文件(如 100 个 PDF),可能会导致某些请求被丢弃或阻塞,进而导致漏传。

解决方案:
  • 控制并发数量:使用Promise.all或其他控制并发请求的方法限制并发上传的数量。例如,可以设置每次只上传 5 到 10 个文件,等待这些文件上传完成后,再上传下一批文件。

代码示例:

js


复制代码
async function uploadFiles(files) {
    const batchSize = 5;  // 每次并发上传的数量
    for (let i = 0; i < files.length; i += batchSize) {
        const batch = files.slice(i, i + batchSize);
        await Promise.all(batch.map(file => uploadFile(file)));
    }
}

async function uploadFile(file) {
    // 实现上传逻辑
}

Promise.all()Promise.race() 都是 JavaScript 中用于处理多个 Promise 的静态方法,但它们的行为有明显区别:

1. Promise.all()

Promise.all() 会等待所有传入的 Promise 都完成(无论成功或失败),然后返回一个新的 Promise,该 Promise 的结果是所有输入 Promise 的结果集合。

  • 成功:如果所有 Promise 都成功,Promise.all() 返回的 Promise 状态为 fulfilled,返回值是一个包含每个 Promise 结果的数组。
  • 失败:如果其中任何一个 Promise 失败(rejected),Promise.all() 会立即返回一个 rejected 的新 Promise,并以第一个失败的 Promise 的原因作为返回值,其他 Promise 的状态将不再被考虑。
适用场景:

Promise.all() 适合用于需要等待多个异步任务全部完成之后再继续后续操作的场景。如果有一个任务失败,就不需要继续执行。

示例:
javascript


复制代码
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve) => setTimeout(resolve, 1000, 'foo'));
const promise3 = Promise.resolve(42);

Promise.all([promise1, promise2, promise3]).then((results) => {
  console.log(results); // 输出: [3, 'foo', 42]
}).catch((error) => {
  console.error(error);
});

如果所有 Promise 都成功,Promise.all() 返回的结果是一个数组,包含每个 Promise 的结果。如果其中有一个失败,比如:

javascript


复制代码
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 1000, 'Error!'));

Promise.all([promise1, promise2]).then((results) => {
  console.log(results);
}).catch((error) => {
  console.error(error); // 输出: 'Error!'
});

由于 promise2 失败,整个 Promise.all() 直接 rejected,输出 'Error!'


2. Promise.race()

Promise.race() 的行为则不同,它只关心哪一个 Promise 首先完成(无论是成功还是失败),然后立即返回该 Promise 的结果,忽略其他未完成的 Promise

  • 成功或失败:无论是成功(fulfilled)还是失败(rejected),Promise.race() 都会返回第一个完成的 Promise,其他 Promise 的状态将被忽略。
适用场景:

Promise.race() 常用于希望某个操作在一组异步任务中率先完成时即可继续后续操作的场景,比如请求超时处理,或者希望第一个响应的任务即被处理的情况。

示例:
javascript


复制代码
const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, 'One'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 500, 'Two'));

Promise.race([promise1, promise2]).then((result) => {
  console.log(result); // 输出: 'Two',因为 promise2 首先完成
}).catch((error) => {
  console.error(error);
});

即使 promise1 最终也会成功,但由于 promise2 更快完成,Promise.race() 会返回 promise2 的结果 'Two'

失败的情况:

如果第一个完成的 Promise 是失败的,则 Promise.race() 会立即 rejected

javascript


复制代码
const promise1 = new Promise((resolve) => setTimeout(resolve, 1000, 'One'));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 500, 'Error!'));

Promise.race([promise1, promise2]).then((result) => {
  console.log(result);
}).catch((error) => {
  console.error(error); // 输出: 'Error!',因为 promise2 先失败
});

3. Promise.all() vs Promise.race()

特性Promise.all()Promise.race()
完成条件所有 Promise都完成(成功或失败)第一个 Promise完成(无论成功或失败)
结果如果所有都成功,返回包含所有结果的数组;如果有一个失败,返回第一个失败的原因返回第一个完成 Promise的结果,其他的 Promise被忽略
适用场景等待所有任务完成后再执行下一步操作,如并行 API 请求的结果处理关心谁先完成,通常用于超时处理、竞争请求等场景
失败处理只要有一个 Promise失败,整个 Promise.all()失败第一个失败的 Promise会导致 Promise.race()失败

4. Promise.all()Promise.race() 的典型应用场景

Promise.all() 示例场景:
  1. 并行请求多个 API 并在所有请求成功后执行下一步处理。
    • 比如,页面加载时需要同时获取用户信息、产品列表和推荐内容,只有当这些请求全部完成后,才能渲染页面。
javascript


复制代码
const userInfo = fetch('/api/user');
const products = fetch('/api/products');
const recommendations = fetch('/api/recommendations');

Promise.all([userInfo, products, recommendations])
  .then(([userInfoResponse, productsResponse, recommendationsResponse]) => {
    // 处理多个请求成功后的逻辑
  })
  .catch((error) => {
    // 处理任何一个请求失败的情况
  });
Promise.race() 示例场景:
  1. 网络请求超时控制:我们可以使用 Promise.race() 来在网络请求超时时返回错误,而不是无限等待。
javascript


复制代码
const fetchData = fetch('/api/data');
const timeout = new Promise((_, reject) => setTimeout(() => reject('Request timed out'), 5000));

Promise.race([fetchData, timeout])
  .then((response) => {
    // 请求成功
  })
  .catch((error) => {
    console.error(error); // 请求超时或请求失败
  });

在这个例子中,Promise.race() 会让最快的 Promise 返回,无论是 fetchData 请求成功还是 timeout 超时。

控制并发数量的任务队列管理,这个需求类似于限流并发执行任务:即保持一定的并发量,任务执行完一个后立刻启动下一个,直到所有任务都执行完毕。这种需求可以用 Promise.race() 来实现有限并发的任务队列,也可以通过手动管理任务队列来实现。

解决方案:并发控制的任务队列

我们可以通过以下步骤实现这个需求:

  1. 限制并发数量,假设并发数为 limit
  2. 一次性启动最多 limit 个任务。
  3. 使用 Promise.race() 来等待当前并发任务中的一个完成。
  4. 当某个任务完成后,从剩下的任务队列中取出下一个任务并执行,继续保持并发数不变。
  5. 当所有任务都完成后,任务队列结束。

实现步骤:

javascript


复制代码
/**
 * 控制并发数的上传函数
 * @param {Array} files - 需要上传的文件数组
 * @param {Function} uploadFn - 单个文件的上传函数,返回 Promise
 * @param {Number} limit - 最大并发数
 * @returns {Promise} - 所有文件上传完成的 Promise
 */
function uploadWithConcurrency(files, uploadFn, limit) {
  let index = 0; // 当前正在处理的任务索引
  const results = []; // 存放结果
  const executing = []; // 当前正在执行的 Promise 数组

  // 包装一个执行任务的函数
  const enqueue = () => {
    if (index === files.length) {
      return Promise.resolve(); // 所有文件已处理完,返回已完成的 Promise
    }

    // 取出下一个要上传的文件
    const file = files[index++];
    const promise = uploadFn(file).then(result => {
      results.push(result); // 上传成功,存储结果
    }).catch(error => {
      results.push(error); // 上传失败,存储错误
    });

    // 将这个 promise 放入执行队列
    executing.push(promise);

    // 当一个 promise 执行完毕后,从执行队列中移除
    const cleanup = promise.then(() => {
      executing.splice(executing.indexOf(promise), 1);
    });

    // 当并发数达到限制时,使用 Promise.race 等待最先完成的任务
    if (executing.length >= limit) {
      return Promise.race(executing).then(() => enqueue());
    } else {
      return enqueue();
    }
  };

  // 开始执行任务
  return Promise.all(Array(limit).fill(null).map(() => enqueue())).then(() => results);
}

// 模拟文件上传函数,返回一个 Promise
function mockUploadFile(file) {
  return new Promise((resolve, reject) => {
    const time = Math.random() * 2000; // 随机时间模拟上传耗时
    setTimeout(() => {
      console.log(`Uploaded: ${file}`);
      resolve(`Result of ${file}`);
    }, time);
  });
}

// 示例文件列表
const files = ['file1', 'file2', 'file3', 'file4', 'file5', 'file6', 'file7'];

// 调用带并发限制的上传函数
uploadWithConcurrency(files, mockUploadFile, 3).then(results => {
  console.log('所有文件上传完成:', results);
});

代码解析:

  1. index:用来跟踪已经上传的文件索引,每次从文件列表 files 中取出一个文件来上传。
  2. results:存放每个文件上传完成后的结果(无论成功还是失败)。
  3. executing:维护当前正在执行的 Promise 列表。每次并发数超过 limit 时,使用 Promise.race 来等待其中最快完成的一个,然后继续启动新的上传任务。
  4. enqueue:是一个递归函数,它会根据 index 不断从 files 中取出下一个文件并上传,同时保证执行中的任务数不超过 limit

流程:

  • 初始时,启动最多 limit 个上传任务。
  • 每当一个任务完成(使用 Promise.race()),立刻启动下一个任务,确保任务执行队列中的任务数不超过并发限制。
  • 当所有任务完成时,返回结果数组,包含每个文件的上传结果。

优点:

  • 控制了并发数量,不会一次性发起过多请求,避免服务器压力过大或浏览器的并发限制问题。
  • 可以灵活设置并发数 limit,根据实际情况动态调整。
  • 使用 Promise.race() 和队列管理的方式,确保上传任务始终保持 limit 的并发量,任务完成一个立刻启动下一个。

扩展:重试机制

如果某些文件上传失败,还可以结合重试机制,在 catch 中对失败的文件进行多次重试,确保上传的可靠性。

文件上传,批量处理,防止漏传

element-plus上传的代码优化处理

<template>
  <el-upload
    v-model:file-list="fileList"
    :http-request="uploadFile"
    :multiple="true"
    :before-upload="beforeUpload"
    :auto-upload="false"
    list-type="text"
  >
    <el-button type="primary">上传文件</el-button>
    <template #tip>
      <div class="el-upload__tip">
        jpg/png files with a size less than 500KB.
      </div>
    </template>
  </el-upload>
  <el-button type="success" @click="submitUpload">提交</el-button>
</template>

<script setup>
import { ref } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';

// 存储文件列表
const fileList = ref([]);

// 批量上传函数
const uploadFilesInBatch = async (files) => {
  const batchSize = 5;  // 每次并发上传的数量
  let successCount = 0; // 统计成功上传的文件数量
  let failureCount = 0; // 统计上传失败的文件数量

  for (let i = 0; i < files.length; i += batchSize) {
    const batch = files.slice(i, i + batchSize);
    const results = await Promise.allSettled(batch.map(file => uploadFile({ file })));

    // 根据结果处理成功和失败的计数
    results.forEach(result => {
      if (result.status === 'fulfilled') {
        successCount++;
      } else {
        failureCount++;
      }
    });
  }

  // 显示上传结果
  ElMessage({
    message: `上传完成!成功:${successCount},失败:${failureCount}`,
    type: failureCount > 0 ? 'warning' : 'success',
    duration: 5000,
  });
};

// 提交上传的逻辑
const submitUpload = async () => {
  if (fileList.value.length === 0) {
    ElMessage.warning('没有文件需要上传.');
    return;
  }

  try {
    // 过滤未成功上传的文件
    const filesToUpload = fileList.value.filter(file => file.status !== 'success');
    await uploadFilesInBatch(filesToUpload);
  } catch (error) {
    ElMessage.error('文件上传过程中出现错误.');
  }
};

// 自定义上传文件逻辑
const uploadFile = ({ file }) => {
  return new Promise(async (resolve, reject) => {
    const formData = new FormData();
    formData.append('file', file.raw); // 使用 file.raw 获取原始文件

    try {
      const response = await axios.post('/service/designWorkorder/importByZip', formData, {
        headers: { 'Content-Type': 'multipart/form-data' },
      });

      if (response.status === 200) {
        file.status = 'success'; // 更新文件状态为成功
        resolve();  // 上传成功,调用 resolve
      } else {
        file.status = 'fail'; // 更新文件状态为失败
        reject(new Error(`文件 ${file.name} 上传失败.`));  // 上传失败,调用 reject
      }
    } catch (error) {
      file.status = 'fail'; // 更新文件状态为失败
      reject(error);  // 捕获任何异常,调用 reject
    }
  });
};

// 上传前限制文件类型和大小
const beforeUpload = (file) => {
  const isJPGOrPNG = file.type === 'image/jpeg' || file.type === 'image/png';
  const isLt500KB = file.size / 1024 < 500;

  if (!isJPGOrPNG) {
    ElMessage.error('仅允许上传 JPG/PNG 文件.');
    return false;
  }

  if (!isLt500KB) {
    ElMessage.error('文件大小必须小于 500KB.');
    return false;
  }

  return true;
};
</script>

<style scoped>
.el-upload__tip {
  color: #909399;
  font-size: 12px;
}
</style>