JavaScript异步函数的优雅封装与实战指南

22 阅读10分钟

前言

在现代前端开发中,异步操作几乎无处不在。从网络请求到文件读写,从定时器到事件处理,JavaScript的执行环境充满了异步场景。在ES6之前,我们主要依靠回调函数来处理异步操作,但这常常导致"回调地狱"的问题,代码变得难以维护和理解。

ES6引入了Promise,ES2017又带来了async/await语法糖,这些特性极大地改善了JavaScript处理异步操作的方式。然而,仅仅了解基础语法是不够的,如何优雅地封装异步函数、如何在不同场景下选择合适的异步处理策略,成为了提升代码质量和开发效率的关键。

本文将从异步函数的基础概念出发,深入探讨异步函数的封装技巧,并通过丰富的实例,展示在各种实际场景中的最佳实践。无论你是前端新手还是有经验的开发者,相信都能从中获得启发。

一、异步函数的基础知识

1.1 从回调函数到Promise再到async/await

JavaScript的异步编程经历了三个主要阶段:回调函数、Promise、async/await。

回调函数是最原始的异步处理方式:

// 回调函数处理异步
function fetchData(callback) {
  setTimeout(() => {
    callback(null, '数据获取成功');
  }, 1000);
}

fetchData((error, data) => {
  if (error) {
    console.error('出错了:', error);
    return;
  }
  console.log('获取到数据:', data);
});

Promise的出现解决了回调地狱问题:

// Promise处理异步
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('数据获取成功');
      // 失败时使用 reject(new Error('获取失败'));
    }, 1000);
  });
}

fetchData()
  .then(data => {
    console.log('获取到数据:', data);
  })
  .catch(error => {
    console.error('出错了:', error);
  });

async/await则提供了更接近同步代码的写法:

// async/await处理异步
async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('数据获取成功');
    }, 1000);
  });
}

async function processData() {
  try {
    const data = await fetchData();
    console.log('获取到数据:', data);
  } catch (error) {
    console.error('出错了:', error);
  }
}

processData();

1.2 async/await的工作原理

async/await是基于Promise的语法糖,它让异步代码看起来更像传统的同步代码。当我们使用async关键字声明一个函数时,这个函数会自动返回一个Promise对象。在async函数内部使用await关键字时,它会暂停函数的执行,等待Promise解析完成后再继续执行。

值得注意的是,await关键字只能在async函数内部使用,而且即使await后面跟着的是一个普通值(不是Promise),JavaScript也会将其包装成一个已解析的Promise。

1.3 异步函数的错误处理

在异步函数中处理错误是非常重要的。通常有两种方式来处理async函数中的错误:

  1. 使用try/catch结构
  2. 使用Promise的catch方法
// 方式1:try/catch
async function processWithTryCatch() {
  try {
    const result = await mightFailOperation();
    console.log('操作成功:', result);
  } catch (error) {
    console.error('捕获到错误:', error);
  }
}

// 方式2:Promise catch
async function processWithPromiseCatch() {
  const result = await mightFailOperation().catch(error => {
    console.error('捕获到错误:', error);
    return '默认值'; // 可选:提供默认值
  });
  console.log('结果:', result);
}

这两种方式各有优缺点,具体使用哪种取决于你的代码结构和错误处理策略。try/catch结构更直观,适合处理整个函数体的错误;而Promise catch更灵活,可以针对特定的异步操作进行错误处理。

二、异步函数的封装技巧

2.1 基础封装模式

封装异步函数的基本原则是:保持接口简洁、处理错误、提供合理的默认值、支持取消操作。

以下是一个基础的异步函数封装模板:

/**
 * 封装异步函数的基础模板
 * @param {Object} options - 配置选项
 * @returns {Promise} - 返回Promise对象
 */
async function wrapAsyncFunction(options = {}) {
  // 设置默认参数
  const { timeout = 30000, retry = 0, ...otherOptions } = options;
  
  let attempts = 0;
  let lastError = null;
  
  // 重试机制
  while (attempts <= retry) {
    try {
      // 创建一个Promise来支持超时设置
      const result = await Promise.race([
        // 实际的异步操作
        actualAsyncOperation(otherOptions),
        // 超时处理
        new Promise((_, reject) => 
          setTimeout(() => reject(new Error(`操作超时(${timeout}ms)`)), timeout)
        )
      ]);
      
      return result;
    } catch (error) {
      lastError = error;
      attempts++;
      
      // 如果已经是最后一次尝试,则抛出错误
      if (attempts > retry) {
        throw lastError;
      }
      
      // 重试前的延迟
      await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
    }
  }
}

/**
 * 实际执行的异步操作
 * @param {Object} options - 操作选项
 * @returns {Promise} - 返回Promise对象
 */
function actualAsyncOperation(options) {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    // resolve(result); 或 reject(new Error('失败原因'));
  });
}

2.2 带取消功能的异步函数封装

在某些场景下,我们可能需要取消正在进行的异步操作,比如用户点击了取消按钮,或者页面跳转导致某个异步请求变得不再必要。

以下是一个支持取消功能的异步函数封装示例:

/**
 * 创建可取消的异步函数
 * @param {Function} asyncFunction - 原始异步函数
 * @returns {Object} - 包含执行函数和取消函数的对象
 */
function createCancelableAsync(asyncFunction) {
  let cancelToken = null;
  
  // 创建取消标记的工厂函数
  function makeCancelToken() {
    const callbacks = [];
    
    return {
      // 添加取消时的回调
      register: (callback) => {
        callbacks.push(callback);
        // 返回取消注册的函数
        return () => {
          const index = callbacks.indexOf(callback);
          if (index > -1) {
            callbacks.splice(index, 1);
          }
        };
      },
      // 执行取消操作
      cancel: (reason = 'Operation canceled') => {
        callbacks.forEach(callback => callback(reason));
      }
    };
  }
  
  // 包装后的异步函数
  async function wrappedFunction(...args) {
    // 取消之前的token(如果存在)
    if (cancelToken) {
      cancelToken.cancel('New operation started');
    }
    
    // 创建新的取消token
    cancelToken = makeCancelToken();
    
    try {
      // 创建一个Promise竞赛
      const result = await Promise.race([
        // 执行原始异步函数
        asyncFunction(...args, cancelToken),
        // 创建一个可被取消的Promise
        new Promise((_, reject) => {
          const unregister = cancelToken.register(reason => {
            reject(new Error(reason));
          });
          
          // 确保取消注册
          cancelToken.register(unregister);
        })
      ]);
      
      return result;
    } finally {
      // 清理
      if (cancelToken) {
        const currentToken = cancelToken;
        // 延迟清理,以便可以链式调用取消函数
        setTimeout(() => {
          if (currentToken === cancelToken) {
            cancelToken = null;
          }
        }, 0);
      }
    }
  }
  
  // 取消函数
  function cancel(reason) {
    if (cancelToken) {
      cancelToken.cancel(reason);
    }
  }
  
  return {
    execute: wrappedFunction,
    cancel
  };
}

// 使用示例
const fetchWithCancel = createCancelableAsync(async (url, options = {}, cancelToken) => {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 注册取消回调
  const unregister = cancelToken.register(() => {
    controller.abort();
  });
  
  try {
    const response = await fetch(url, {
      ...options,
      signal
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
  } finally {
    // 取消注册
    unregister();
  }
});

// 使用示例
const { execute, cancel } = fetchWithCancel;

// 执行异步操作
async function runFetch() {
  try {
    const data = await execute('https://api.example.com/data');
    console.log('获取数据成功:', data);
  } catch (error) {
    if (error.message.includes('canceled')) {
      console.log('操作被取消');
    } else {
      console.error('获取数据失败:', error);
    }
  }
}

runFetch();

// 取消操作(比如在按钮点击事件中)
// cancel('用户取消操作');

2.3 异步函数的并发控制

在处理大量异步操作时(比如批量上传文件、并行请求多个接口),直接使用Promise.all可能会导致同时发起过多请求,给服务器带来压力,甚至导致浏览器或服务器报错。这时,我们需要控制异步操作的并发数量。

以下是一个实现并发控制的异步函数封装示例:

/**
 * 控制异步函数的并发执行
 * @param {Array} tasks - 任务数组
 * @param {Function} taskHandler - 任务处理函数
 * @param {Number} concurrency - 最大并发数
 * @returns {Promise} - 返回包含所有任务结果的Promise
 */
async function asyncPool(tasks, taskHandler, concurrency) {
  const results = []; // 存储所有任务的结果
  const executing = new Set(); // 存储正在执行的任务
  const taskQueue = [...tasks]; // 任务队列的副本
  
  // 创建一个执行器函数
  async function executeNext() {
    // 如果队列已空且没有正在执行的任务,则返回
    if (taskQueue.length === 0 && executing.size === 0) {
      return;
    }
    
    // 当有空闲槽位且任务队列不为空时,执行下一个任务
    while (executing.size < concurrency && taskQueue.length > 0) {
      const task = taskQueue.shift();
      const taskIndex = tasks.indexOf(task);
      
      // 创建一个Promise来表示任务的执行
      const taskPromise = Promise.resolve().then(() => taskHandler(task));
      
      // 将任务添加到正在执行的集合中
      executing.add(taskPromise);
      
      // 处理任务完成后的逻辑
      taskPromise.then(
        // 成功处理
        (result) => {
          results[taskIndex] = result; // 按原顺序存储结果
        },
        // 错误处理
        (error) => {
          results[taskIndex] = { error }; // 存储错误信息
          console.error(`Task ${taskIndex} failed:`, error);
        }
      ).finally(() => {
        // 从正在执行的集合中移除已完成的任务
        executing.delete(taskPromise);
        // 继续执行下一个任务
        executeNext();
      });
    }
  }
  
  // 开始执行任务
  await executeNext();
  
  // 等待所有正在执行的任务完成
  while (executing.size > 0) {
    await Promise.race([...executing]);
  }
  
  return results;
}

// 使用示例
async function uploadFiles(files) {
  // 文件上传函数
  async function uploadFile(file) {
    // 模拟文件上传
    return new Promise((resolve) => {
      setTimeout(() => {
        console.log(`上传完成: ${file.name}`);
        resolve({ success: true, fileName: file.name });
      }, Math.random() * 3000 + 1000);
    });
  }
  
  // 控制最大并发数为3
  const results = await asyncPool(files, uploadFile, 3);
  console.log('所有上传任务完成:', results);
  return results;
}

// 模拟文件数组
const mockFiles = [
  { name: 'file1.jpg' },
  { name: 'file2.png' },
  { name: 'file3.pdf' },
  { name: 'file4.doc' },
  { name: 'file5.txt' }
];

// 执行上传
uploadFiles(mockFiles);

2.4 带超时处理的异步函数封装

在网络请求或其他异步操作中,设置超时机制是一种常见的最佳实践。这可以防止操作无限期地等待,提高应用的响应性和用户体验。

以下是一个带超时处理的异步函数封装示例:

/**
 * 为异步函数添加超时处理
 * @param {Function} asyncFunction - 要执行的异步函数
 * @param {Number} timeoutMs - 超时时间(毫秒)
 * @param {String} timeoutMessage - 超时错误消息
 * @returns {Function} - 包装后的异步函数
 */
function withTimeout(asyncFunction, timeoutMs = 30000, timeoutMessage = `操作超时(${timeoutMs}ms)`) {
  return async function(...args) {
    // 创建一个超时Promise
    const timeoutPromise = new Promise((_, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(timeoutMessage));
      }, timeoutMs);
      
      // 清理函数,防止内存泄漏
      timeoutPromise.clearTimeout = () => clearTimeout(timeoutId);
    });
    
    try {
      // 使用Promise.race进行超时控制
      const result = await Promise.race([
        asyncFunction.apply(this, args),
        timeoutPromise
      ]);
      
      return result;
    } finally {
      // 确保清理超时定时器
      if (timeoutPromise.clearTimeout) {
        timeoutPromise.clearTimeout();
      }
    }
  };
}

// 使用示例
async function fetchData(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  return response.json();
}

// 添加10秒超时
const fetchDataWithTimeout = withTimeout(fetchData, 10000, '数据加载超时,请检查网络');

// 使用带超时的函数
async function loadUserData(userId) {
  try {
    const userData = await fetchDataWithTimeout(`https://api.example.com/users/${userId}`);
    console.log('用户数据:', userData);
    return userData;
  } catch (error) {
    console.error('获取用户数据失败:', error.message);
    // 提供降级数据或执行其他恢复操作
    return { id: userId, name: '未知用户', fallback: true };
  }
}

loadUserData(123);

三、常见使用场景的最佳实践

3.1 错误处理策略

在异步编程中,错误处理是一个关键环节。良好的错误处理不仅可以提高代码的健壮性,还能改善用户体验。下面我们将详细介绍异步函数中的错误处理策略。

3.1.1 try-catch 错误处理

try-catch 是处理异步函数错误的最基本方法,它可以捕获 async 函数中抛出的所有同步和异步错误:

/**
 * 使用 try-catch 处理异步函数错误
 */
async function processUserData(userId) {
  try {
    // 尝试获取用户数据
    const userData = await fetchUserData(userId);
    
    // 尝试处理用户数据
    const processedData = processUserInfo(userData);
    
    // 尝试保存处理后的数据
    await saveProcessedData(processedData);
    
    console.log('用户数据处理完成');
    return processedData;
  } catch (error) {
    // 捕获所有错误并进行统一处理
    console.error('处理用户数据时出错:', error);
    
    // 根据错误类型进行不同的处理
    if (error.name === 'AbortError') {
      console.log('操作被用户取消');
    } else if (error.response?.status === 401) {
      console.log('认证失败,需要重新登录');
      // 可以在这里跳转到登录页面
    } else if (error.response?.status === 404) {
      console.log('请求的资源不存在');
    } else {
      console.log('发生未知错误');
    }
    
    // 可以选择抛出错误让上层处理
    // throw new Error(`处理失败: ${error.message}`);
    
    // 或者返回默认值
    return { id: userId, name: '未知用户', error: error.message };
  }
}

3.1.2 Promise.catch() 链式调用

在处理多个连续的异步操作时,可以使用 Promise.catch() 链式调用来集中处理错误:

/**
 * 使用 Promise.catch() 处理链式异步操作错误
 */
async function fetchAndProcessData() {
  // 链式调用多个异步操作
  const result = await fetchInitialData()
    .then(data => processStep1(data))
    .then(result1 => processStep2(result1))
    .then(result2 => processStep3(result2))
    .catch(error => {
      // 集中处理所有步骤中可能出现的错误
      console.error('处理数据时出错:', error);
      
      // 根据错误来源进行不同的处理
      if (error.step === 'fetch') {
        console.log('获取初始数据失败');
      } else if (error.step === 'step1') {
        console.log('第一步处理失败');
      }
      
      // 返回默认值或重新抛出错误
      return { success: false, error: error.message };
    });
  
  console.log('最终处理结果:', result);
  return result;
}

3.1.3 错误处理的最佳实践

  1. 始终捕获异步函数的错误:不捕获错误可能导致应用崩溃或行为异常
  2. 提供有意义的错误信息:帮助调试和问题定位
  3. 区分错误类型:根据错误类型提供不同的处理策略
  4. 使用自定义错误类:创建特定场景的错误类型,便于错误分类和处理
  5. 错误日志记录:在生产环境中记录详细的错误信息,有助于问题排查
  6. 用户友好的错误提示:向用户展示易于理解的错误信息,而不是技术错误细节

以下是一个使用自定义错误类的示例:

/**
 * 自定义错误类
 */
class ApiError extends Error {
  constructor(message, statusCode, errorCode) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.errorCode = errorCode;
  }
}

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

/**
 * 使用自定义错误类处理异步函数错误
 */
async function submitUserData(userData) {
  try {
    // 验证用户数据
    validateUserData(userData);
    
    // 提交用户数据到API
    const response = await apiClient.post('/users', userData);
    
    if (!response.ok) {
      // 根据HTTP状态码抛出特定的API错误
      throw new ApiError(
        response.statusText,
        response.status,
        response.data?.errorCode || 'UNKNOWN_ERROR'
      );
    }
    
    return response.data;
  } catch (error) {
    // 根据错误类型进行不同的处理
    if (error instanceof ValidationError) {
      console.error('数据验证失败:', error.fieldErrors);
      return { success: false, validationErrors: error.fieldErrors };
    } else if (error instanceof ApiError) {
      console.error(`API错误 [${error.statusCode}]: ${error.message}`);
      return { success: false, apiError: error };
    } else {
      console.error('未知错误:', error);
      return { success: false, error: error.message };
    }
  }
}

/**
 * 验证用户数据的函数
 */
function validateUserData(userData) {
  const errors = {};
  
  if (!userData.name || userData.name.trim().length === 0) {
    errors.name = '用户名不能为空';
  }
  
  if (!userData.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
    errors.email = '请输入有效的邮箱地址';
  }
  
  if (!userData.password || userData.password.length < 6) {
    errors.password = '密码长度不能少于6个字符';
  }
  
  // 如果有验证错误,抛出ValidationError
  if (Object.keys(errors).length > 0) {
    throw new ValidationError('用户数据验证失败', errors);
  }
}

3.2 网络请求的封装与错误处理

网络请求是前端开发中最常见的异步操作场景。一个好的网络请求封装应该包括:请求配置、错误处理、重试机制、超时控制等功能。

以下是一个完整的网络请求封装示例:

/**
 * 网络请求工具类
 */
class ApiService {
  constructor(baseUrl, defaultHeaders = {}) {
    this.baseUrl = baseUrl;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders
    };
    this.maxRetries = 3;
  }
  
  /**
   * 创建完整的URL
   */
  createUrl(endpoint) {
    return `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
  }
  
  /**
   * 基础请求方法
   */
  async request(method, endpoint, options = {}) {
    const { 
      headers = {}, 
      body = null, 
      retries = this.maxRetries,
      timeout = 30000,
      ...fetchOptions
    } = options;
    
    const url = this.createUrl(endpoint);
    const requestOptions = {
      method,
      headers: { ...this.defaultHeaders, ...headers },
      ...fetchOptions
    };
    
    // 如果有请求体,且内容类型是JSON,则序列化
    if (body && requestOptions.headers['Content-Type'] === 'application/json') {
      requestOptions.body = JSON.stringify(body);
    }
    
    let attempt = 0;
    let lastError = null;
    
    // 重试循环
    while (attempt <= retries) {
      try {
        // 添加超时控制
        const response = await Promise.race([
          fetch(url, requestOptions),
          new Promise((_, reject) => 
            setTimeout(() => reject(new Error(`请求超时(${timeout}ms)`)), timeout)
          )
        ]);
        
        if (!response.ok) {
          // 处理HTTP错误
          const errorText = await response.text();
          const error = new Error(`HTTP错误 ${response.status}: ${errorText || response.statusText}`);
          error.status = response.status;
          
          // 对于特定状态码,可能需要特殊处理
          if (response.status === 401) {
            // 未授权,可能需要刷新token
            throw new Error('未授权,请重新登录');
          }
          
          throw error;
        }
        
        // 尝试解析JSON响应
        try {
          return await response.json();
        } catch (jsonError) {
          // 如果不是JSON响应,则返回文本
          return await response.text();
        }
      } catch (error) {
        lastError = error;
        attempt++;
        
        // 判断是否需要重试(网络错误或特定状态码)
        const shouldRetry = attempt <= retries && (
          !error.status || 
          [408, 429, 500, 502, 503, 504].includes(error.status)
        );
        
        if (!shouldRetry) {
          throw lastError;
        }
        
        // 指数退避策略
        const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
        console.log(`请求失败,${delay}ms后重试(${attempt}/${retries})...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
    
    // 理论上不会执行到这里,但为了类型安全添加
    throw lastError || new Error('请求失败');
  }
  
  // 快捷方法
  async get(endpoint, options = {}) {
    return this.request('GET', endpoint, options);
  }
  
  async post(endpoint, data, options = {}) {
    return this.request('POST', endpoint, { ...options, body: data });
  }
  
  async put(endpoint, data, options = {}) {
    return this.request('PUT', endpoint, { ...options, body: data });
  }
  
  async delete(endpoint, options = {}) {
    return this.request('DELETE', endpoint, options);
  }
  
  // 批量请求方法
  async batchRequests(requests, concurrency = 3) {
    const results = [];
    const executing = new Set();
    const requestQueue = [...requests];
    
    async function executeNext() {
      if (requestQueue.length === 0 && executing.size === 0) {
        return;
      }
      
      while (executing.size < concurrency && requestQueue.length > 0) {
        const [method, endpoint, options] = requestQueue.shift();
        const requestPromise = this.request(method, endpoint, options);
        
        executing.add(requestPromise);
        
        requestPromise
          .then(result => {
            results.push({ success: true, result });
          })
          .catch(error => {
            results.push({ success: false, error: error.message });
          })
          .finally(() => {
            executing.delete(requestPromise);
            executeNext.call(this);
          });
      }
    }
    
    await executeNext.call(this);
    
    // 等待所有请求完成
    while (executing.size > 0) {
      await Promise.race([...executing]);
    }
    
    return results;
  }
}

// 使用示例
const apiService = new ApiService('https://api.example.com', {
  'X-API-Key': 'your-api-key'
});

// 发起GET请求
async function getUserProfile(userId) {
  try {
    const userProfile = await apiService.get(`/users/${userId}`, {
      timeout: 15000,
      retries: 2
    });
    console.log('用户资料:', userProfile);
    return userProfile;
  } catch (error) {
    console.error('获取用户资料失败:', error.message);
    // 处理错误逻辑
  }
}

// 发起POST请求
async function createUser(userData) {
  try {
    const newUser = await apiService.post('/users', userData);
    console.log('创建用户成功:', newUser);
    return newUser;
  } catch (error) {
    console.error('创建用户失败:', error.message);
    // 处理错误逻辑
  }
}

// 批量请求
async function fetchMultipleResources() {
  const requests = [
    ['GET', '/users'],
    ['GET', '/posts'],
    ['GET', '/comments'],
    ['GET', '/tags'],
    ['GET', '/categories']
  ];
  
  const results = await apiService.batchRequests(requests, 2);
  console.log('批量请求结果:', results);
  return results;
}

// 调用示例
getUserProfile(1);
createUser({ name: 'John Doe', email: 'john@example.com' });
fetchMultipleResources();

3.2 表单提交与验证

表单处理是另一个常见的异步操作场景,特别是在涉及服务器验证和提交数据时。一个好的表单处理方案应该包括:客户端验证、异步提交、加载状态管理、错误处理等。

以下是一个表单提交与验证的封装示例:

/**
 * 表单处理工具类
 */
class FormHandler {
  constructor(formElement, options = {}) {
    this.form = formElement;
    this.options = {
      validateOnChange: true,
      validateOnBlur: true,
      ...options
    };
    this.validators = new Map();
    this.errors = new Map();
    this.isSubmitting = false;
    this.submitCallback = null;
    
    this.init();
  }
  
  /**
   * 初始化表单事件监听
   */
  init() {
    // 监听表单提交
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
    
    // 监听输入变化和失焦事件
    const inputs = this.form.querySelectorAll('input, select, textarea');
    inputs.forEach(input => {
      if (this.options.validateOnChange) {
        input.addEventListener('input', this.handleInputChange.bind(this, input));
      }
      
      if (this.options.validateOnBlur) {
        input.addEventListener('blur', this.handleInputBlur.bind(this, input));
      }
    });
  }
  
  /**
   * 添加字段验证器
   */
  addValidator(fieldName, validator, errorMessage) {
    if (!this.validators.has(fieldName)) {
      this.validators.set(fieldName, []);
    }
    this.validators.get(fieldName).push({ validator, errorMessage });
    return this;
  }
  
  /**
   * 验证单个字段
   */
  validateField(fieldName) {
    const field = this.form.elements[fieldName];
    if (!field || !this.validators.has(fieldName)) {
      return true;
    }
    
    const value = field.value;
    const validators = this.validators.get(fieldName);
    
    for (const { validator, errorMessage } of validators) {
      let isValid = true;
      
      // 根据验证器类型执行不同的验证
      if (typeof validator === 'function') {
        isValid = validator(value, field);
      } else if (validator instanceof RegExp) {
        isValid = validator.test(value);
      } else if (typeof validator === 'boolean') {
        isValid = validator;
      }
      
      if (!isValid) {
        this.setError(fieldName, errorMessage);
        return false;
      }
    }
    
    this.clearError(fieldName);
    return true;
  }
  
  /**
   * 验证整个表单
   */
  validateForm() {
    let isValid = true;
    
    // 验证所有有验证器的字段
    for (const fieldName of this.validators.keys()) {
      if (!this.validateField(fieldName)) {
        isValid = false;
      }
    }
    
    return isValid;
  }
  
  /**
   * 设置字段错误
   */
  setError(fieldName, errorMessage) {
    this.errors.set(fieldName, errorMessage);
    this.updateErrorDisplay(fieldName, errorMessage);
  }
  
  /**
   * 清除字段错误
   */
  clearError(fieldName) {
    this.errors.delete(fieldName);
    this.updateErrorDisplay(fieldName, '');
  }
  
  /**
   * 更新错误显示
   */
  updateErrorDisplay(fieldName, errorMessage) {
    const field = this.form.elements[fieldName];
    if (!field) return;
    
    // 查找对应的错误显示元素(通常是field的兄弟元素或某个特定容器)
    let errorElement = field.nextElementSibling;
    while (errorElement && !errorElement.classList.contains('error-message')) {
      errorElement = errorElement.nextElementSibling;
    }
    
    // 如果没有错误元素,则创建一个
    if (!errorElement) {
      errorElement = document.createElement('div');
      errorElement.className = 'error-message';
      field.parentNode.insertBefore(errorElement, field.nextSibling);
    }
    
    // 更新错误信息
    errorElement.textContent = errorMessage;
    errorElement.style.display = errorMessage ? 'block' : 'none';
    
    // 添加/移除错误样式类
    if (errorMessage) {
      field.classList.add('error');
    } else {
      field.classList.remove('error');
    }
  }
  
  /**
   * 获取表单数据
   */
  getFormData() {
    const formData = new FormData(this.form);
    const data = {};
    
    for (const [key, value] of formData.entries()) {
      // 处理复选框和单选按钮
      if (this.form.elements[key]?.type === 'checkbox') {
        if (!Array.isArray(data[key])) {
          data[key] = [];
        }
        data[key].push(value);
      } else {
        data[key] = value;
      }
    }
    
    return data;
  }
  
  /**
   * 处理输入变化
   */
  handleInputChange(input) {
    this.validateField(input.name);
  }
  
  /**
   * 处理输入失焦
   */
  handleInputBlur(input) {
    this.validateField(input.name);
  }
  
  /**
   * 处理表单提交
   */
  async handleSubmit(event) {
    event.preventDefault();
    
    // 防止重复提交
    if (this.isSubmitting) return;
    
    // 验证表单
    if (!this.validateForm()) {
      console.log('表单验证失败', Object.fromEntries(this.errors));
      return;
    }
    
    // 设置提交状态
    this.isSubmitting = true;
    this.form.dispatchEvent(new CustomEvent('form:submit:start'));
    
    try {
      // 获取表单数据
      const formData = this.getFormData();
      
      // 执行提交回调
      if (typeof this.submitCallback === 'function') {
        const result = await this.submitCallback(formData);
        this.form.dispatchEvent(new CustomEvent('form:submit:success', { detail: result }));
      }
    } catch (error) {
      console.error('表单提交失败:', error);
      this.form.dispatchEvent(new CustomEvent('form:submit:error', { detail: error }));
    } finally {
      // 恢复提交状态
      this.isSubmitting = false;
      this.form.dispatchEvent(new CustomEvent('form:submit:end'));
    }
  }
  
  /**
   * 设置提交处理函数
   */
  onSubmit(callback) {
    this.submitCallback = callback;
    return this;
  }
  
  /**
   * 重置表单
   */
  reset() {
    this.form.reset();
    this.errors.clear();
    
    // 清除所有错误显示
    for (const fieldName of this.validators.keys()) {
      this.updateErrorDisplay(fieldName, '');
    }
  }
  
  /**
   * 设置表单数据
   */
  setFormData(data) {
    for (const [key, value] of Object.entries(data)) {
      const field = this.form.elements[key];
      if (field) {
        if (field.type === 'checkbox' || field.type === 'radio') {
          if (Array.isArray(value)) {
            // 处理多个复选框值
            const checkboxes = this.form.querySelectorAll(`input[name="${key}"]`);
            checkboxes.forEach(checkbox => {
              checkbox.checked = value.includes(checkbox.value);
            });
          } else {
            // 处理单个复选框或单选按钮
            field.checked = field.value === String(value);
          }
        } else {
          field.value = value;
        }
      }
    }
  }
}

// 使用示例
// 假设HTML中有一个id为'userForm'的表单
// const formElement = document.getElementById('userForm');
// 为了演示,这里创建一个模拟的表单对象
const formElement = {
  addEventListener: () => {},
  dispatchEvent: () => {},
  querySelectorAll: () => [],
  elements: {
    name: { value: '', classList: { add: () => {}, remove: () => {} } },
    email: { value: '', classList: { add: () => {}, remove: () => {} } },
    password: { value: '', classList: { add: () => {}, remove: () => {} } }
  },
  reset: () => {}
};

const userFormHandler = new FormHandler(formElement, {
  validateOnChange: true,
  validateOnBlur: true
});

// 添加验证规则
userFormHandler
  .addValidator('name', val => val.trim().length > 0, '用户名不能为空')
  .addValidator('name', val => val.trim().length <= 50, '用户名不能超过50个字符')
  .addValidator('email', val => val.trim().length > 0, '邮箱不能为空')
  .addValidator('email', /^[^\s@]+@[^\s@]+\.[^\s@]+$/, '请输入有效的邮箱地址')
  .addValidator('password', val => val.length >= 6, '密码长度不能少于6个字符')
  .addValidator('password', val => /[A-Z]/.test(val), '密码必须包含大写字母')
  .addValidator('password', val => /[0-9]/.test(val), '密码必须包含数字');

// 设置提交处理函数
userFormHandler.onSubmit(async (formData) => {
  console.log('提交的表单数据:', formData);
  
  // 模拟异步提交
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('表单提交成功');
      resolve({ success: true, message: '用户注册成功' });
    }, 1500);
  });
});

// 监听表单事件
formElement.addEventListener('form:submit:start', () => {
  console.log('开始提交表单');
  // 可以显示加载指示器
});

formElement.addEventListener('form:submit:success', (event) => {
  console.log('表单提交成功结果:', event.detail);
  // 处理成功逻辑,如显示成功消息、跳转页面等
  alert(event.detail.message);
  userFormHandler.reset();
});

formElement.addEventListener('form:submit:error', (event) => {
  console.log('表单提交失败:', event.detail);
  // 处理错误逻辑,如显示错误消息
  alert('提交失败,请稍后再试');
});

formElement.addEventListener('form:submit:end', () => {
  console.log('表单提交结束');
  // 隐藏加载指示器
});

四、异步函数的高级应用

4.1 异步迭代器与生成器

在处理大量异步数据时,异步迭代器和生成器提供了一种优雅的解决方案。它们允许我们像处理同步数据一样处理异步数据流,避免一次性加载大量数据导致的性能问题。

异步迭代器和生成器的工作原理:

  1. 异步生成器函数使用 async function* 语法定义,内部可以使用 yieldawait
  2. 异步迭代器实现了 Symbol.asyncIterator 方法,返回包含 next() 方法的对象
  3. next() 方法返回的是一个 Promise,解析为 {value, done} 格式的对象
  4. 可以使用 for-await-of 循环来遍历异步迭代器

以下是一个异步迭代器和生成器的示例:

/**
 * 异步生成器函数示例:模拟分页加载数据
 * @param {Number} pageSize - 每页数据量
 * @param {Number} totalItems - 总数据量
 * @param {Number} delay - 模拟网络延迟(毫秒)
 */
async function* paginatedDataLoader(pageSize = 10, totalItems = 100, delay = 500) {
  let currentPage = 1;
  const totalPages = Math.ceil(totalItems / pageSize);
  
  while (currentPage <= totalPages) {
    // 模拟异步加载数据
    const data = await new Promise(resolve => {
      setTimeout(() => {
        const startIndex = (currentPage - 1) * pageSize;
        const endIndex = Math.min(startIndex + pageSize, totalItems);
        
        // 生成模拟数据
        const pageData = Array.from({ length: endIndex - startIndex }, (_, i) => ({
          id: startIndex + i + 1,
          name: `Item ${startIndex + i + 1}`,
          page: currentPage
        }));
        
        resolve(pageData);
      }, delay);
    });
    
    // yield 当前页数据
    yield data;
    
    currentPage++;
  }
}

/**
 * 使用 for-await-of 遍历异步生成器
 */
async function processPaginatedData() {
  console.log('开始处理分页数据...');
  let totalProcessed = 0;
  
  try {
    // 使用 for-await-of 遍历异步生成器
    for await (const pageData of paginatedDataLoader(15, 50, 300)) {
      console.log(`处理第 ${pageData[0].page} 页数据,共 ${pageData.length} 条`);
      // 处理数据...
      totalProcessed += pageData.length;
      
      // 可以在这里添加进度反馈
      console.log(`已处理: ${totalProcessed}/50 条数据`);
    }
    
    console.log('所有数据处理完成!');
  } catch (error) {
    console.error('处理数据时出错:', error);
  }
}

// 执行示例
processPaginatedData();

异步迭代器和生成器在以下场景特别有用:

  1. 处理分页API数据
  2. 处理大量数据流(如文件上传/下载)
  3. 实时数据更新(如WebSocket消息处理)
  4. 处理计算密集型任务,避免阻塞主线程

4.2 异步函数的性能优化

虽然 async/await 语法让异步代码更易读,但在处理大量异步操作时,仍需要注意性能优化。以下是一些优化技巧:

/**
 * 异步函数性能优化示例
 */

// 1. 并行执行独立的异步操作
async function parallelOperations() {
  console.time('Parallel');
  
  // 使用 Promise.all 并行执行多个独立的异步操作
  const [result1, result2, result3] = await Promise.all([
    fetchDataFromSource1(),
    fetchDataFromSource2(),
    fetchDataFromSource3()
  ]);
  
  console.timeEnd('Parallel');
  return { result1, result2, result3 };
}

// 2. 使用 Promise.race 实现超时控制
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const { signal } = controller;
  
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    const response = await fetch(url, { signal });
    clearTimeout(timeoutId); // 清除超时定时器
    
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`请求超时(${timeoutMs}ms)`);
    }
    throw error;
  }
}

// 3. 使用记忆化缓存重复的异步结果
function memoizeAsync(asyncFunction) {
  const cache = new Map();
  
  return async function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log('缓存命中,返回缓存结果');
      return cache.get(key);
    }
    
    try {
      const result = await asyncFunction.apply(this, args);
      cache.set(key, result);
      return result;
    } catch (error) {
      // 注意:通常不缓存错误结果
      throw error;
    }
  };
}

// 使用记忆化优化数据获取函数
const memoizedFetchData = memoizeAsync(async (id) => {
  console.log(`从服务器获取数据 ID: ${id}`);
  // 模拟网络请求
  await new Promise(resolve => setTimeout(resolve, 1000));
  return { id, data: `数据内容 ${id}` };
});

// 4. 使用节流和防抖优化频繁调用的异步函数
function throttleAsync(func, limit) {
  let inThrottle;
  
  return async function(...args) {
    if (!inThrottle) {
      const result = await func.apply(this, args);
      inThrottle = true;
      
      setTimeout(() => {
        inThrottle = false;
      }, limit);
      
      return result;
    }
  };
}

function debounceAsync(func, delay) {
  let timeoutId;
  
  return async function(...args) {
    clearTimeout(timeoutId);
    
    return new Promise(resolve => {
      timeoutId = setTimeout(async () => {
        try {
          const result = await func.apply(this, args);
          resolve(result);
        } catch (error) {
          console.error('Debounced function error:', error);
          // 可以决定是 reject 还是 resolve 错误
          resolve({ error: error.message });
        }
      }, delay);
    });
  };
}

// 5. 使用 requestAnimationFrame 优化视觉相关的异步更新
async function updateWithAnimationFrame(data) {
  return new Promise(resolve => {
    requestAnimationFrame(() => {
      // 执行视觉更新
      updateUI(data);
      resolve();
    });
  });
}

function updateUI(data) {
  // 更新DOM或执行其他视觉操作
  console.log('UI已更新:', data);
}

// 模拟函数
async function fetchDataFromSource1() { await new Promise(r => setTimeout(r, 1000)); return '数据1'; }
async function fetchDataFromSource2() { await new Promise(r => setTimeout(r, 1500)); return '数据2'; }
async function fetchDataFromSource3() { await new Promise(r => setTimeout(r, 800)); return '数据3'; }

// 使用示例
async function runOptimizationExamples() {
  // 测试并行操作
  console.log('测试并行操作:');
  await parallelOperations();
  
  // 测试记忆化
  console.log('\n测试记忆化:');
  await memoizedFetchData(1); // 第一次调用,从服务器获取
  await memoizedFetchData(1); // 第二次调用,使用缓存
  
  // 测试节流
  console.log('\n测试节流:');
  const throttledFunction = throttleAsync(async (id) => {
    console.log(`执行节流函数 ID: ${id}`);
    await new Promise(r => setTimeout(r, 500));
    return `结果 ${id}`;
  }, 2000);
  
  await throttledFunction(1);
  await throttledFunction(2); // 会被忽略,因为还在节流期间
  
  // 等待节流时间过去
  await new Promise(r => setTimeout(r, 2000));
  await throttledFunction(3); // 会执行,因为节流时间已过
}

runOptimizationExamples();

4.3 异步函数在前端框架中的应用

现代前端框架如 React、Vue 和 Angular 都广泛使用异步函数来处理数据获取、副作用等操作。以下是一些在主流框架中使用异步函数的示例:

React 中的异步函数应用

import React, { useState, useEffect, useCallback } from 'react';

/**
 * React 组件中使用异步函数获取数据
 */
function UserProfile({ userId }) {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  // 使用 useCallback 避免不必要的重新创建函数
  const fetchUserData = useCallback(async () => {
    if (!userId) return;
    
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(`https://api.example.com/users/${userId}`);
      
      if (!response.ok) {
        throw new Error(`获取用户数据失败: ${response.status}`);
      }
      
      const data = await response.json();
      setUserData(data);
    } catch (err) {
      setError(err.message);
      console.error('获取用户数据时出错:', err);
    } finally {
      setLoading(false);
    }
  }, [userId]); // 依赖项数组,只有 userId 变化时才重新创建函数
  
  // 使用 useEffect 处理副作用
  useEffect(() => {
    fetchUserData();
    
    // 清理函数(在组件卸载或依赖项变化前执行)
    return () => {
      // 这里可以取消正在进行的请求
      // 例如:abortController.abort();
    };
  }, [fetchUserData]); // 依赖于 fetchUserData 函数
  
  if (loading) {
    return <div className="loading">加载中...</div>;
  }
  
  if (error) {
    return <div className="error">{error}</div>;
  }
  
  return (
    <div className="user-profile">
      <h2>{userData?.name || '未知用户'}</h2>
      <p>邮箱: {userData?.email}</p>
      <p>注册时间: {new Date(userData?.createdAt).toLocaleDateString()}</p>
    </div>
  );
}

/**
 * 使用 React Hooks 和异步函数的自定义 Hook
 */
function useAsyncOperation(operation, dependencies = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const execute = useCallback(async (...args) => {
    setLoading(true);
    setError(null);
    
    try {
      const result = await operation(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [operation]);
  
  // 如果需要自动执行(可选)
  useEffect(() => {
    if (dependencies.length > 0) {
      execute(...dependencies);
    }
  }, [execute, ...dependencies]);
  
  return { execute, data, loading, error };
}

/**
 * 使用自定义 Hook 获取用户列表
 */
function UserList() {
  const { execute: fetchUsers, data: users, loading, error } = useAsyncOperation(
    async () => {
      const response = await fetch('https://api.example.com/users');
      if (!response.ok) throw new Error('获取用户列表失败');
      return response.json();
    },
    [] // 空数组表示只在组件挂载时执行一次
  );
  
  return (
    <div>
      <button onClick={fetchUsers} disabled={loading}>
        {loading ? '加载中...' : '刷新用户列表'}
      </button>
      
      {error && <div className="error">{error.message}</div>}
      
      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name}</li>
        )) || '暂无数据'}
      </ul>
    </div>
  );
}

Vue 3 中的异步函数应用

import { ref, onMounted, computed } from 'vue';

/**
 * Vue 3 组合式 API 中使用异步函数
 */
export default {
  name: 'ProductList',
  props: {
    category: {
      type: String,
      default: 'all'
    }
  },
  setup(props) {
    const products = ref([]);
    const loading = ref(false);
    const error = ref(null);
    
    // 异步函数获取产品数据
    const fetchProducts = async (category = 'all') => {
      loading.value = true;
      error.value = null;
      
      try {
        const url = `https://api.example.com/products${category !== 'all' ? `?category=${category}` : ''}`;
        const response = await fetch(url);
        
        if (!response.ok) {
          throw new Error('获取产品数据失败');
        }
        
        products.value = await response.json();
      } catch (err) {
        error.value = err.message;
        console.error('获取产品时出错:', err);
      } finally {
        loading.value = false;
      }
    };
    
    // 监听 category 属性变化
    watch(() => props.category, (newCategory) => {
      fetchProducts(newCategory);
    });
    
    // 组件挂载时获取数据
    onMounted(() => {
      fetchProducts(props.category);
    });
    
    // 计算属性:过滤产品(仅示例)
    const filteredProducts = computed(() => {
      if (props.category === 'all') return products.value;
      return products.value.filter(p => p.category === props.category);
    });
    
    // 刷新数据方法
    const refreshData = () => {
      fetchProducts(props.category);
    };
    
    // 暴露给模板的数据和方法
    return {
      products: filteredProducts,
      loading,
      error,
      refreshData
    };
  }
};

/**
 * Vue 3 中的异步生命周期钩子示例
 */
export default {
  name: 'AsyncComponent',
  async setup() {
    // 注意:setup 本身可以是异步函数,但这样会导致组件成为异步组件
    // 异步组件需要在父组件中使用 Suspense 包裹
    
    const data = await fetchInitialData();
    
    return {
      data
    };
  }
};

// 父组件中使用 Suspense
const ParentComponent = {
  template: `
    <Suspense>
      <template #default>
        <AsyncComponent />
      </template>
      <template #fallback>
        <div>组件加载中...</div>
      </template>
    </Suspense>
  `
};

4.4 使用 async/await 处理事件循环

理解 JavaScript 的事件循环对于正确使用异步函数至关重要。事件循环负责协调执行代码、处理事件和执行异步操作的结果。

以下是一些关于事件循环和异步函数的关键点:

  1. async/await 是基于 Promise 和微任务的
  2. await 表达式会暂停函数执行,直到 Promise 解析完成
  3. Promise 的回调(如 .then()、.catch()、.finally())属于微任务
  4. setTimeout、setInterval、I/O 操作等属于宏任务
  5. 微任务会在当前宏任务执行完成后立即执行,而宏任务则需要等待下一轮事件循环

以下是一个展示事件循环中 async/await 执行顺序的示例:

/**
 * 展示事件循环中异步函数的执行顺序
 */
async function eventLoopDemo() {
  console.log('1. 开始执行');
  
  // 宏任务
  setTimeout(() => {
    console.log('6. setTimeout 回调执行 (宏任务)');
  }, 0);
  
  // 微任务 - 通过 Promise
  Promise.resolve().then(() => {
    console.log('4. Promise then 回调执行 (微任务)');
  });
  
  // await 表达式 - 会创建微任务
  await new Promise(resolve => {
    console.log('2. Promise 立即执行部分');
    resolve('resolved');
  });
  
  console.log('5. await 之后的代码执行');
  
  // 创建另一个微任务
  Promise.resolve().then(() => {
    console.log('7. await 之后的 Promise then 回调 (微任务)');
  });
  
  console.log('3. 函数同步部分结束');
}

// 执行示例
console.log('启动事件循环演示');
eventLoopDemo();
console.log('事件循环演示函数调用完成');

/*
预期输出顺序:
启动事件循环演示
1. 开始执行
2. Promise 立即执行部分
事件循环演示函数调用完成
4. Promise then 回调执行 (微任务)
5. await 之后的代码执行
3. 函数同步部分结束
7. await 之后的 Promise then 回调 (微任务)
6. setTimeout 回调执行 (宏任务)
*/

/**
 * 异步函数和事件循环的实际应用:防抖处理
 */
function debounceWithAsync(func, wait) {
  let timeout;
  
  return async function(...args) {
    const later = () => {
      clearTimeout(timeout);
      // 执行原始函数并返回结果
      return func.apply(this, args);
    };
    
    clearTimeout(timeout);
    
    // 创建一个新的 Promise 包装 setTimeout
    return new Promise(resolve => {
      timeout = setTimeout(async () => {
        try {
          const result = await later();
          resolve(result);
        } catch (error) {
          // 处理错误,或者将错误 reject 给调用者
          console.error('Debounced function error:', error);
          resolve({ error: error.message });
        }
      }, wait);
    });
  };
}

// 使用示例
const searchApi = async (query) => {
  console.log(`搜索: ${query}`);
  // 模拟 API 请求延迟
  await new Promise(resolve => setTimeout(resolve, 500));
  return { results: [`结果 ${query} 1`, `结果 ${query} 2`] };
};

// 创建防抖版本的搜索函数
const debouncedSearch = debounceWithAsync(searchApi, 300);

// 模拟用户输入
async function simulateUserInput() {
  console.log('模拟用户输入...');
  
  // 快速连续调用防抖函数
  const result1 = await debouncedSearch('java'); // 可能会被取消
  const result2 = await debouncedSearch('javascript'); // 可能会被取消
  const result3 = await debouncedSearch('js async'); // 最终执行的搜索
  
  console.log('最终搜索结果:', result3);
}

simulateUserInput();

最后,创作不易请允许我插播一则自己开发的“数规规-排五助手”(有各种趋势分析)小程序广告,感兴趣可以微信小程序体验放松放松,程序员也要有点娱乐生活,搞不好就中个排列五了呢?

感兴趣可以微信扫码如下小程序二维码体验,或者搜索“数规规排五助手”体验体验

如果觉得本文有用,欢迎点个赞👍+收藏⭐+关注支持我吧!