进度通知的异步实现

在现代Web开发中,异步编程已成为JavaScript的核心概念。当处理耗时操作时,如文件上传、数据处理或API调用,向用户提供进度反馈至关重要。本文将探讨如何在JavaScript中实现进度通知的异步模式。

回调函数与进度通知

传统的回调函数模式也可以实现进度通知,尽管这种方式在现代开发中已不太推荐:

javascript 复制代码
function processData(data, callback, progressCallback) {
  let total = data.length;
  let processed = 0;
  
  data.forEach((item, index) => {
    // 模拟处理每个项目的耗时
    setTimeout(() => {
      processItem(item);
      processed++;
      // 调用进度回调
      progressCallback(processed / total);
      
      if (processed === total) {
        callback('处理完成');
      }
    }, index * 100);
  });
}

// 使用示例
processData(
  [1, 2, 3, 4, 5],
  result => console.log(result),
  progress => console.log(`进度: ${Math.round(progress * 100)}%`)
);

Promise与进度通知

Promise本身不直接支持进度通知,但我们可以通过扩展来实现:

javascript 复制代码
class ProgressPromise {
  constructor(executor) {
    this._progressHandlers = [];
    this._promise = new Promise((resolve, reject) => {
      executor(resolve, reject, progress => {
        this._progressHandlers.forEach(handler => handler(progress));
      });
    });
  }
  
  then(onFulfilled, onRejected) {
    return this._promise.then(onFulfilled, onRejected);
  }
  
  catch(onRejected) {
    return this._promise.catch(onRejected);
  }
  
  progress(handler) {
    this._progressHandlers.push(handler);
    return this;
  }
}

// 使用示例
function asyncTask() {
  return new ProgressPromise((resolve, reject, progress) => {
    let percent = 0;
    const interval = setInterval(() => {
      percent += 10;
      progress(percent);
      if (percent >= 100) {
        clearInterval(interval);
        resolve('完成');
      }
    }, 500);
  });
}

asyncTask()
  .progress(p => console.log(`进度: ${p}%`))
  .then(result => console.log(result));

使用Async/Await与事件发射器

结合async/await和事件发射器可以实现更清晰的进度通知:

javascript 复制代码
const EventEmitter = require('events');

class AsyncProcessor extends EventEmitter {
  async process(data) {
    const total = data.length;
    let processed = 0;
    
    for (const item of data) {
      await this.processItem(item);
      processed++;
      this.emit('progress', processed / total);
    }
    
    return '处理完成';
  }
  
  processItem(item) {
    return new Promise(resolve => {
      setTimeout(() => {
        console.log(`处理项目: ${item}`);
        resolve();
      }, 500);
    });
  }
}

// 使用示例
const processor = new AsyncProcessor();
processor.on('progress', progress => {
  console.log(`进度: ${Math.round(progress * 100)}%`);
});

processor.process([1, 2, 3, 4, 5])
  .then(result => console.log(result));

现代浏览器API中的进度通知

许多现代浏览器API原生支持进度通知:

javascript 复制代码
// Fetch API上传文件带进度
async function uploadFile(file) {
  const formData = new FormData();
  formData.append('file', file);
  
  const xhr = new XMLHttpRequest();
  
  xhr.upload.onprogress = event => {
    if (event.lengthComputable) {
      const percent = Math.round((event.loaded / event.total) * 100);
      console.log(`上传进度: ${percent}%`);
    }
  };
  
  return new Promise((resolve, reject) => {
    xhr.onload = () => resolve(xhr.response);
    xhr.onerror = () => reject(new Error('上传失败'));
    xhr.open('POST', '/upload', true);
    xhr.send(formData);
  });
}

// 或者使用fetch + ReadableStream
async function processStreamWithProgress(response) {
  const reader = response.body.getReader();
  const contentLength = +response.headers.get('Content-Length');
  let receivedLength = 0;
  let chunks = [];
  
  while(true) {
    const {done, value} = await reader.read();
    
    if (done) break;
    
    chunks.push(value);
    receivedLength += value.length;
    console.log(`接收进度: ${Math.round((receivedLength / contentLength) * 100)}%`);
  }
  
  return new Blob(chunks);
}

最佳实践

  1. 适度更新频率:进度更新不应太频繁,以免影响性能
  2. 平滑显示:考虑使用动画或节流来平滑进度显示
  3. 错误处理:确保在操作失败时提供适当的反馈
  4. 取消支持:为长时间运行的操作提供取消功能
  5. 预估时间:在可能的情况下,提供剩余时间估计

通过合理运用这些技术,你可以为用户提供流畅的异步操作体验,显著提升应用的用户友好性。