前言
在现代前端开发中,异步操作几乎无处不在。从网络请求到文件读写,从定时器到事件处理,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函数中的错误:
- 使用try/catch结构
- 使用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 错误处理的最佳实践
- 始终捕获异步函数的错误:不捕获错误可能导致应用崩溃或行为异常
- 提供有意义的错误信息:帮助调试和问题定位
- 区分错误类型:根据错误类型提供不同的处理策略
- 使用自定义错误类:创建特定场景的错误类型,便于错误分类和处理
- 错误日志记录:在生产环境中记录详细的错误信息,有助于问题排查
- 用户友好的错误提示:向用户展示易于理解的错误信息,而不是技术错误细节
以下是一个使用自定义错误类的示例:
/**
* 自定义错误类
*/
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 异步迭代器与生成器
在处理大量异步数据时,异步迭代器和生成器提供了一种优雅的解决方案。它们允许我们像处理同步数据一样处理异步数据流,避免一次性加载大量数据导致的性能问题。
异步迭代器和生成器的工作原理:
- 异步生成器函数使用
async function*语法定义,内部可以使用yield和await - 异步迭代器实现了
Symbol.asyncIterator方法,返回包含next()方法的对象 next()方法返回的是一个 Promise,解析为{value, done}格式的对象- 可以使用
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();
异步迭代器和生成器在以下场景特别有用:
- 处理分页API数据
- 处理大量数据流(如文件上传/下载)
- 实时数据更新(如WebSocket消息处理)
- 处理计算密集型任务,避免阻塞主线程
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 的事件循环对于正确使用异步函数至关重要。事件循环负责协调执行代码、处理事件和执行异步操作的结果。
以下是一些关于事件循环和异步函数的关键点:
- async/await 是基于 Promise 和微任务的
- await 表达式会暂停函数执行,直到 Promise 解析完成
- Promise 的回调(如 .then()、.catch()、.finally())属于微任务
- setTimeout、setInterval、I/O 操作等属于宏任务
- 微任务会在当前宏任务执行完成后立即执行,而宏任务则需要等待下一轮事件循环
以下是一个展示事件循环中 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();
最后,创作不易请允许我插播一则自己开发的“数规规-排五助手”(有各种趋势分析)小程序广告,感兴趣可以微信小程序体验放松放松,程序员也要有点娱乐生活,搞不好就中个排列五了呢?
感兴趣可以微信扫码如下小程序二维码体验,或者搜索“数规规排五助手”体验体验
如果觉得本文有用,欢迎点个赞👍+收藏⭐+关注支持我吧!