魯斯前端布魯斯前端

文章中英模式

布魯斯前端JS面試題目 - 實作 Promise Pool 並發控制

學習如何實作 Promise Pool 控制並發執行的非同步任務,解決高併發問題,提升前端應用性能與面試競爭力。

影片縮圖

懶得看文章?那就來看影片吧

Promise Pool 概述

Promise Pool(Promise 池)是一種控制並發執行 Promise 的技術,主要用於限制同時執行的非同步任務數量。在處理大量非同步操作時(如 API 請求、檔案處理等),它可以有效防止系統過載並優化資源使用。

Promise Pool 的主要特點

  • 1. 控制並發數:限制同時執行的 Promise 數量
  • 2. 動態調度:一個任務完成後自動開始下一個
  • 3. 按序提交:任務按順序提交,但不保證按順序完成
  • 4. 錯誤處理:可以處理單個任務的失敗而不影響其他任務
  • 5. 資源優化:防止系統資源過度消耗

Promise Pool 運作原理圖解

任務佇列: [Task1, Task2, Task3, Task4, Task5, Task6, ...]
┌───────────────────────────────────┐
│         Promise Pool (並發數=3)    │
├───────────┬───────────┬───────────┤
│  Task1    │  Task2    │  Task3    │ ← 同時執行的任務
│  執行中   │  執行中   │  執行中   │
├───────────┴───────────┴───────────┤
│                                   │
│  Task4, Task5, ... 等待執行       │
│                                   │
└───────────────────────────────────┘
┌───────────────────────────────────┐
│         Promise Pool (並發數=3)    │
├───────────┬───────────┬───────────┤
│  Task4    │  Task2    │  Task3    │ ← Task1完成後,
│  執行中   │  執行中   │  執行中   │   Task4開始執行
└───────────┴───────────┴───────────┘

當設定並發數為3時,系統一次最多執行3個任務。當其中一個任務完成後,池中會立即從佇列取出下一個任務開始執行,保持並發數量恆定,直到所有任務都完成。

在前端面試中,實作一個簡單的 Promise Pool 是測試候選人對 Promise、非同步處理和執行控制理解的好題目。

基本實作:Promise Pool

以下是一個基本的 Promise Pool 實作,限制同時運行的 Promise 數量:

const promisePool = async function(functions, n) {
    return new Promise((resolve) => {
        let inProgress = 0;  // 正在執行的 Promise 數量
        let functionIndex = 0;  // 待執行函數的索引
        
        // 定義協助執行下一個函數的函數
        function helper() {
            // 如果所有函數都已完成,解析 Promise
            if (functionIndex >= functions.length && inProgress === 0) {
                resolve();
                return;
            }
            
            // 當有空閒額度且還有函數待執行時,執行下一個
            while (inProgress < n && functionIndex < functions.length) {
                // 取得下一個函數
                const fn = functions[functionIndex++];
                inProgress++; // 增加執行中的 Promise 數量
                
                // 執行函數
                fn().then(() => {
                    inProgress--; // 減少執行中的 Promise 數量
                    helper(); // 嘗試執行下一個
                });
            }
        }
        
        // 開始執行
        helper();
    });
};

使用這個 Promise Pool 的例子:

// 模擬異步任務的函數
const createTask = (id, delay) => {
  return () => new Promise(resolve => {
    console.log(`任務 ${id} 開始執行,預計耗時 ${delay}ms`);
    setTimeout(() => {
      console.log(`任務 ${id} 完成`);
      resolve(`任務 ${id} 的結果`);
    }, delay);
  });
};

// 創建一系列任務
const tasks = [
  createTask(1, 1000),
  createTask(2, 500),
  createTask(3, 2000),
  createTask(4, 800),
  createTask(5, 1500),
  createTask(6, 1000),
  createTask(7, 600),
  createTask(8, 3000)
];

// 使用 Promise Pool,最大並發數為 3
promisePool(tasks, 3).then(() => {
  console.log('所有任務完成');
});

// 輸出(時間僅供參考):
// 任務 1 開始執行,預計耗時 1000ms
// 任務 2 開始執行,預計耗時 500ms
// 任務 3 開始執行,預計耗時 2000ms
// 任務 2 完成
// 任務 4 開始執行,預計耗時 800ms
// 任務 1 完成
// 任務 5 開始執行,預計耗時 1500ms
// 任務 4 完成
// 任務 6 開始執行,預計耗時 1000ms
// ...
// 所有任務完成

實際應用場景

Promise Pool 在前端開發中有很多實際應用場景:

1. 批量上傳檔案

// 批量上傳檔案示例
async function uploadFilesWithLimit(files, maxConcurrent) {
  const uploadTasks = files.map(file => {
    return async () => {
      const formData = new FormData();
      formData.append('file', file);
      
      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });
      
      if (!response.ok) {
        throw new Error(`Upload failed for ${file.name}`);
      }
      
      return await response.json();
    };
  });
  
  return promisePoolWithResults(uploadTasks, maxConcurrent);
}

// 使用例子
document.getElementById('fileInput').addEventListener('change', async (event) => {
  const files = event.target.files;
  
  try {
    const results = await uploadFilesWithLimit(Array.from(files), 3);
    console.log('所有檔案上傳完成', results);
  } catch (error) {
    console.error('上傳過程發生錯誤', error);
  }
});

2. 分頁資料批次處理

// 分頁資料批次處理示例
async function processAllPages(totalPages, maxConcurrent) {
  // 創建頁面處理任務
  const pageTasks = Array.from({ length: totalPages }, (_, i) => {
    const page = i + 1;
    
    return async () => {
      console.log(`處理第 ${page}`);
      const response = await fetch(`/api/data?page=${page}`);
      
      if (!response.ok) {
        throw new Error(`Failed to fetch page ${page}`);
      }
      
      const data = await response.json();
      
      // 進行資料處理...
      return processPagesData(data);
    };
  });
  
  // 使用 Promise Pool 限制並發
  return promisePoolWithResults(pageTasks, maxConcurrent);
}

// 使用例子
processAllPages(100, 5)
  .then(results => {
    console.log('所有頁面處理完成');
    // 合併或進一步處理結果...
  })
  .catch(error => {
    console.error('處理過程發生錯誤', error);
  });