在 React 中封装 Promise,核心是结合 React 的生命周期/ Hooks 特性(如 useEffect 处理异步时机、useState 管理异步状态),封装通用的异步请求逻辑(如 API 调用),同时解决重复请求、错误处理、加载状态管理等问题。以下是从「基础封装」到「通用 Hooks 封装」的完整实现方案,适配 React 函数组件(主流开发模式)。
先实现最基础的 Promise 封装,直接在组件中结合 useEffect 处理异步逻辑,适合简单场景(如单个组件的独立请求)。
useState 管理 loading、data、error 状态;useEffect 触发异步请求(依赖项变化时重新请求);AbortController 实现请求取消(组件卸载时终止未完成请求)。import React, { useState, useEffect } from 'react';
// 模拟 API 请求(实际项目中替换为 axios/fetch 真实请求)
const fetchData = (params, signal) => {
// 返回 Promise 对象
return new Promise((resolve, reject) => {
// 模拟网络延迟
const timer = setTimeout(() => {
if (params?.id === 1) {
resolve({ code: 200, data: { name: 'React Promise 封装', age: 3 } });
} else {
reject(new Error('请求失败:参数错误'));
}
}, 1000);
// 监听取消信号(组件卸载时触发)
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new Error('请求已取消'));
});
});
};
const BasicPromiseDemo = () => {
// 管理异步状态
const [state, setState] = useState({
loading: false,
data: null,
error: null,
});
// 触发请求的函数
const fetchDataHandler = async (params = { id: 1 }) => {
// 重置状态 + 开启加载
setState(prev => ({ ...prev, loading: true, error: null }));
// 创建取消控制器
const controller = new AbortController();
const signal = controller.signal;
try {
// 执行 Promise 请求
const response = await fetchData(params, signal);
setState({ loading: false, data: response.data, error: null });
} catch (err) {
// 忽略取消请求的错误(组件卸载时触发,无需展示)
if (err.message !== '请求已取消') {
setState({ loading: false, data: null, error: err.message });
}
}
// 返回取消函数(供组件卸载时调用)
return () => controller.abort();
};
// 组件挂载时触发请求,卸载时取消请求
useEffect(() => {
const cancelRequest = fetchDataHandler();
// 组件卸载时取消请求(清理副作用)
return () => cancelRequest();
}, []);
// 手动触发请求(如按钮点击)
const handleRetry = () => {
fetchDataHandler({ id: 1 });
};
const { loading, data, error } = state;
return (
<div style={{ padding: '20px' }}>
<h3>基础 Promise 封装示例</h3>
{loading && <div>加载中...</div>}
{error && <div style={{ color: 'red' }}>错误:{error}</div>}
{data && (
<div>
<p>名称:{data.name}</p>
<p>版本:{data.age}</p>
</div>
)}
<button onClick={handleRetry} disabled={loading}>
重新请求
</button>
</div>
);
};
export default BasicPromiseDemo;对于多个组件需要异步请求的场景,封装成通用 usePromise Hooks,统一管理异步逻辑,减少重复代码。
loading/data/error 状态;import { useState, useRef, useEffect } from 'react';
/**
* 通用 Promise 异步请求 Hooks
* @param {Function} requestFn - 异步请求函数(需返回 Promise)
* @param {Array} deps - 依赖项数组(依赖变化时自动重新请求)
* @param {Object} options - 配置项(manual: 是否手动触发,默认 false)
* @returns {Object} { loading, data, error, run, cancel } - 状态和操作函数
*/
const usePromise = (requestFn, deps = [], options = {}) => {
const { manual = false } = options;
// 状态管理
const [state, setState] = useState({
loading: false,
data: null,
error: null,
});
// 存储取消控制器(用于组件卸载或重复请求时取消上一次请求)
const controllerRef = useRef(null);
// 防止重复请求的锁
const requestLock = useRef(false);
// 取消请求的函数
const cancel = () => {
if (controllerRef.current) {
controllerRef.current.abort();
controllerRef.current = null;
}
};
// 核心请求函数(支持传递参数)
const run = async (...args) => {
// 防止重复请求
if (requestLock.current) return;
requestLock.current = true;
// 重置状态 + 开启加载
setState(prev => ({ ...prev, loading: true, error: null }));
try {
// 创建新的取消控制器(取消上一次未完成的请求)
cancel();
const controller = new AbortController();
const signal = controller.signal;
controllerRef.current = controller;
// 执行请求函数(传递参数和取消信号)
const result = await requestFn(...args, signal);
setState({ loading: false, data: result, error: null });
return result; // 返回结果供外部使用
} catch (error) {
// 忽略取消请求的错误
if (error.name !== 'AbortError') {
setState({ loading: false, data: null, error: error.message || '请求失败' });
return Promise.reject(error); // 外部可捕获错误
}
} finally {
requestLock.current = false; // 释放锁
}
};
// 依赖变化时自动请求(非手动模式下)
useEffect(() => {
if (!manual) {
run();
}
// 组件卸载时取消请求
return () => {
cancel();
};
}, deps); // 依赖项变化时重新请求
return {
...state,
run, // 手动触发请求
cancel, // 手动取消请求
};
};
export default usePromise;import React from 'react';
import usePromise from './usePromise';
// 1. 定义真实请求函数(如 API 调用,支持 axios/fetch)
// 示例 1:Fetch API(原生)
const fetchUser = async (userId, signal) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`, { signal });
if (!response.ok) throw new Error('用户数据请求失败');
return response.json();
};
// 示例 2:Axios(需安装 axios)
// import axios from 'axios';
// const fetchUser = async (userId, signal) => {
// const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`, {
// signal: signal, // Axios 支持 AbortController 信号
// });
// return response.data;
// };
const UserComponent = () => {
// 2. 使用 usePromise Hooks 封装请求
const {
loading,
data: user,
error,
run: fetchUserById,
} = usePromise(
fetchUser, // 请求函数
[], // 依赖项:空数组 = 组件挂载时只请求一次(类似 useEffect([]))
{ manual: true } // 手动触发模式(不自动请求)
);
// 3. 手动触发请求(如按钮点击)
const handleFetchUser = () => {
fetchUserById(1); // 传递参数给请求函数(userId=1)
};
return (
<div style={{ padding: '20px' }}>
<h3>用户信息</h3>
<button onClick={handleFetchUser} disabled={loading}>
{loading ? '请求中...' : '获取用户数据'}
</button>
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
{user && (
<div style={{ marginTop: '10px' }}>
<p>姓名:{user.name}</p>
<p>邮箱:{user.email}</p>
<p>电话:{user.phone}</p>
</div>
)}
</div>
);
};
export default UserComponent;在 usePromise 基础上扩展,支持并发请求、结果缓存,满足更复杂场景(如列表查询、频繁切换参数请求)。
import { useState, useRef, useEffect } from 'react';
const usePromiseAdvance = (requestFn, deps = [], options = {}) => {
const { manual = false, cache = false } = options;
const [state, setState] = useState({ loading: false, data: null, error: null });
const controllerRef = useRef(null);
const requestLock = useRef(false);
const cacheRef = useRef({}); // 缓存结果(key: 参数序列化,value: 数据)
// 取消请求
const cancel = () => {
if (controllerRef.current) {
controllerRef.current.abort();
controllerRef.current = null;
}
};
// 序列化参数(作为缓存 key)
const serializeParams = (...args) => JSON.stringify(args);
// 核心请求函数
const run = async (...args) => {
if (requestLock.current) return;
requestLock.current = true;
// 缓存key
const cacheKey = serializeParams(...args);
// 有缓存且开启缓存模式,直接返回缓存数据
if (cache && cacheRef.current[cacheKey]) {
setState({ loading: false, data: cacheRef.current[cacheKey], error: null });
requestLock.current = false;
return cacheRef.current[cacheKey];
}
setState(prev => ({ ...prev, loading: true, error: null }));
try {
cancel();
const controller = new AbortController();
const signal = controller.signal;
controllerRef.current = controller;
const result = await requestFn(...args, signal);
// 缓存结果
if (cache) cacheRef.current[cacheKey] = result;
setState({ loading: false, data: result, error: null });
return result;
} catch (error) {
if (error.name !== 'AbortError') {
setState({ loading: false, data: null, error: error.message || '请求失败' });
return Promise.reject(error);
}
} finally {
requestLock.current = false;
}
};
// 自动请求
useEffect(() => {
if (!manual) {
run();
}
return () => cancel();
}, deps);
// 清除缓存的方法
const clearCache = (key) => {
if (key) {
delete cacheRef.current[serializeParams(key)];
} else {
cacheRef.current = {};
}
};
return {
...state,
run,
cancel,
clearCache, // 暴露清除缓存的方法
};
};
export default usePromiseAdvance;const CacheDemo = () => {
const { loading, data, run, clearCache } = usePromiseAdvance(
fetchUser,
[],
{ manual: true, cache: true } // 开启缓存
);
return (
<div>
<button onClick={() => run(1)} disabled={loading}>
获取用户1(缓存)
</button>
<button onClick={() => clearCache(1)} style={{ marginLeft: '10px' }}>
清除用户1缓存
</button>
{data && <p>{data.name}</p>}
</div>
);
};特性 | 基础封装 | usePromise Hooks | 高级扩展 Hooks |
|---|---|---|---|
加载/错误/数据状态 | ✅ | ✅ | ✅ |
请求取消(防内存泄漏) | ✅ | ✅ | ✅ |
防止重复请求 | ❌ | ✅ | ✅ |
手动/自动触发请求 | ❌ | ✅ | ✅ |
依赖项变化重新请求 | ❌ | ✅ | ✅ |
结果缓存 | ❌ | ❌ | ✅ |
清除缓存 | ❌ | ❌ | ✅ |
requestFn 需支持接收 signal 参数(用于取消请求),Axios v0.22+、Fetch API 原生支持 AbortController。useEffect,deps 数组中的变量变化时会自动重新请求,避免依赖项缺失导致的闭包问题。manual: true 时,组件挂载不会自动请求,需通过 run 方法手动触发(适合按钮点击、表单提交等场景)。如果项目需要更复杂的异步管理(如请求拦截、响应统一处理、并发控制),可直接使用成熟库:
axios.create 封装全局请求拦截器,统一处理请求头、错误码;本教程的封装方案适合学习 Promise 原理和 React Hooks 实战,或中小型项目无需引入额外库的场景。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。