数字花园

WEB 开发 · 创意编程

JavaScript异步编程模式详解

JavaScript异步编程模式详解

JavaScript异步编程模式详解

JavaScript是单线程语言,但通过异步编程,我们可以处理耗时操作而不阻塞主线程。让我们深入了解JavaScript异步编程的演进历程。

回调函数(Callbacks)

最初的异步解决方案是回调函数:

function fetchData(callback) {
  setTimeout(() => {
    const data = { id: 1, name: '用户数据' };
    callback(null, data);
  }, 1000);
}

fetchData((error, data) => {
  if (error) {
    console.error('获取数据失败:', error);
  } else {
    console.log('数据:', data);
  }
});

回调地狱问题

当需要处理多个异步操作时,回调函数会导致”回调地狱”:

fetchUser(userId, (userError, user) => {
  if (userError) {
    handleError(userError);
    return;
  }
  
  fetchUserPosts(user.id, (postsError, posts) => {
    if (postsError) {
      handleError(postsError);
      return;
    }
    
    fetchPostComments(posts[0].id, (commentsError, comments) => {
      if (commentsError) {
        handleError(commentsError);
        return;
      }
      
      // 终于可以处理数据了...
      console.log('用户:', user);
      console.log('帖子:', posts);
      console.log('评论:', comments);
    });
  });
});

Promise

Promise为异步编程带来了更优雅的解决方案:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = Math.random() > 0.3;
      if (success) {
        resolve({ id: 1, name: '用户数据' });
      } else {
        reject(new Error('网络错误'));
      }
    }, 1000);
  });
}

// 使用Promise
fetchData()
  .then(data => {
    console.log('数据:', data);
    return processData(data);
  })
  .then(processedData => {
    console.log('处理后的数据:', processedData);
  })
  .catch(error => {
    console.error('错误:', error.message);
  });

Promise链式调用

Promise可以很好地解决回调地狱问题:

fetchUser(userId)
  .then(user => {
    console.log('用户:', user);
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    console.log('帖子:', posts);
    return fetchPostComments(posts[0].id);
  })
  .then(comments => {
    console.log('评论:', comments);
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

Promise.all 并行处理

当需要同时处理多个独立的异步操作时:

const promises = [
  fetchUser(userId),
  fetchUserSettings(userId),
  fetchUserNotifications(userId)
];

Promise.all(promises)
  .then(([user, settings, notifications]) => {
    console.log('用户信息:', user);
    console.log('用户设置:', settings);
    console.log('用户通知:', notifications);
  })
  .catch(error => {
    console.error('至少一个请求失败:', error);
  });

Async/Await

ES2017引入的async/await使异步代码看起来像同步代码:

async function loadUserData(userId) {
  try {
    const user = await fetchUser(userId);
    console.log('用户:', user);
    
    const posts = await fetchUserPosts(user.id);
    console.log('帖子:', posts);
    
    const comments = await fetchPostComments(posts[0].id);
    console.log('评论:', comments);
    
    return { user, posts, comments };
  } catch (error) {
    console.error('加载用户数据失败:', error);
    throw error;
  }
}

// 使用async函数
loadUserData(123)
  .then(data => {
    console.log('所有数据加载完成:', data);
  })
  .catch(error => {
    console.error('处理失败:', error);
  });

并行处理的async/await

async function loadAllData(userId) {
  try {
    // 并行执行多个异步操作
    const [user, settings, notifications] = await Promise.all([
      fetchUser(userId),
      fetchUserSettings(userId),
      fetchUserNotifications(userId)
    ]);
    
    return { user, settings, notifications };
  } catch (error) {
    console.error('加载数据失败:', error);
    throw error;
  }
}

错误处理最佳实践

1. 统一错误处理

class ApiError extends Error {
  constructor(message, status) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
  }
}

async function apiRequest(url) {
  try {
    const response = await fetch(url);
    
    if (!response.ok) {
      throw new ApiError(`HTTP错误: ${response.status}`, response.status);
    }
    
    return await response.json();
  } catch (error) {
    if (error instanceof ApiError) {
      // API错误
      console.error('API请求失败:', error.message);
    } else {
      // 网络错误或其他错误
      console.error('请求异常:', error.message);
    }
    throw error;
  }
}

2. 重试机制

async function retryRequest(fn, maxRetries = 3, delay = 1000) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      console.log(`尝试 ${i + 1} 失败:`, error.message);
      
      if (i === maxRetries - 1) {
        throw error; // 最后一次重试失败,抛出错误
      }
      
      // 等待一段时间后重试
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// 使用重试机制
async function fetchDataWithRetry() {
  return retryRequest(() => apiRequest('/api/data'), 3, 2000);
}

实际应用示例

文件上传进度追踪

async function uploadFileWithProgress(file, onProgress) {
  return new Promise((resolve, reject) => {
    const formData = new FormData();
    formData.append('file', file);
    
    const xhr = new XMLHttpRequest();
    
    // 监听上传进度
    xhr.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        const progress = (event.loaded / event.total) * 100;
        onProgress(progress);
      }
    });
    
    xhr.addEventListener('load', () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`上传失败: ${xhr.status}`));
      }
    });
    
    xhr.addEventListener('error', () => {
      reject(new Error('网络错误'));
    });
    
    xhr.open('POST', '/api/upload');
    xhr.send(formData);
  });
}

// 使用示例
async function handleFileUpload(file) {
  try {
    const result = await uploadFileWithProgress(file, (progress) => {
      console.log(`上传进度: ${progress.toFixed(2)}%`);
    });
    
    console.log('上传成功:', result);
  } catch (error) {
    console.error('上传失败:', error.message);
  }
}

性能优化建议

  1. 避免不必要的await: 如果不需要等待结果,不要使用await
  2. 合理使用Promise.all: 对于独立的异步操作,使用并行处理
  3. 控制并发数量: 避免同时发起过多请求
  4. 使用适当的错误处理: 不要忽略错误,但也不要过度处理

异步编程是JavaScript的核心特性之一。通过理解和掌握这些模式,我们可以编写出更高效、更可维护的代码。