手写中实现并学习ahooks——useRequest
前言
最近业务没有之前紧张了,也是消失了一段时间,也总结了一些之前业务上的问题。
和同事沟通也是发现普通的async + await + 封装api在复杂业务场景下针对于请求的业务逻辑比较多,也是推荐我去学习一波ahooks,由于问题起源于请求,因此作者也是直接从 useRequest 开始看起。
附ahooks useRequest链接:
https://ahooks-v2.js.org/zh-CN/hooks/async/
实现
话不多说,手写直接开始,参考几个比较常用的 useRequest 能力来一个个实现吧。
基础版(雏形)
先上代码:
useRequest.ts
interface UseRequestOptionsProps {/** 请求参数*/initialData?: object;/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const { initialData, onSuccess } = options;useEffect(() => {setLoading(true);setError(null);setData(null);request();}, [requestFn]);// useRequest业务逻辑const request = async () => {try {const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}};return { data, loading, error };
};export default useRequest;
使用
const { data, loading, error } = useRequest(queryCompensatoryOrderSituation,{initialData: {compensatoryId,}onSuccess: (res) => {console.log('success request!', res);},},
);
useRequest 对于请求函数的写法并无过多要求,只要是一个异步function且返回一个promise对象,即可传入useRequest的第一个参数中,而第二个参数则是一系列的可选配置项,雏形版本我们暂时只支持onSuccess。
手动触发
代码改造后:
useRequest.ts
interface UseRequestOptionsProps {/** 手动开启*/manual?: boolean;/** 请求参数*/initialData?: object;/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const { manual, initialData, onSuccess } = options;useEffect(() => {setLoading(true);setError(null);setData(null);!manual && request();}, [manual]);// useRequest业务逻辑const request = async () => {try {const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}};return { data, loading, error, request };
};export default useRequest;
使用
const { data, loading, error, request } = useRequest(queryCompensatoryOrderSituation,{manual: true,initialData: {compensatoryId,},onSuccess: (res) => {console.log('success request!', res);},},
);request();
手动执行的逻辑主要是根据manual参数砍掉useRequest mount阶段的渲染请求,把执行请求的能力暴露出去,在页面中去手动调用request()来触发。
轮询与手动取消
代码改造后:
useRequest.ts
interface UseRequestOptionsProps {/** 手动开启*/manual?: boolean;/** 请求参数*/initialData?: object;/** 轮询*/pollingInterval?: number | null;/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const status = useRef<boolean>(false);const pollingIntervalTimer = useRef<NodeJS.Timer | null>(null);const { manual, initialData, pollingInterval, onSuccess } = options;useEffect(() => {setLoading(true);setError(null);setData(null);!manual && request();}, [manual]);// useRequest业务逻辑const request = async () => {try {!status.current && (status.current = true);if (pollingInterval && status.current) {pollingIntervalTimer.current = setTimeout(() => {status.current && request();}, pollingInterval);}const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}};return { data, loading, error, request, cancel };
};// 取消
const cancel = () => {if (pollingIntervalTimer.current) {clearTimeout(pollingIntervalTimer.current);pollingIntervalTimer.current = null;status.current && (status.current = false);}
};export default useRequest;
使用
const { data, loading, error, request, cancel } = useRequest(queryCompensatoryOrderSituation,{manual: true,initialData: {compensatoryId,},pollingInterval: 1000,onSuccess: (res) => {console.log('success request!', res);},},
);request();...
// 轮询到理想数据后
cancel();
轮询的支持在hook中主要用到了timer setTimeout的递归思路,同时给出一个status状态值判断是否在轮询中,当调用端执行cancel(),status则为false;当轮询开始,则status为true。
而cancel()的能力主要也是取消了timer的递归请求逻辑,并且轮询的业务场景和manual: true配合很多。
依赖请求(串型请求)
代码改造后:
useRequest.ts
interface UseRequestOptionsProps {/** 手动开启*/manual?: boolean;/** 请求参数*/initialData?: object;/** 轮询*/pollingInterval?: number | null;/** 准备,用于依赖请求*/ready?: boolean;/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const status = useRef<boolean>(false);const pollingIntervalTimer = useRef<NodeJS.Timer | null>(null);const {manual,initialData,pollingInterval,ready = true,onSuccess,} = options;useEffect(() => {setLoading(true);setError(null);setData(null);!manual && ready && request();}, [manual, ready]);// useRequest业务逻辑const request = async () => {try {!status.current && (status.current = true);if (pollingInterval && status.current) {pollingIntervalTimer.current = setTimeout(() => {status.current && request();}, pollingInterval);}const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}};return { data, loading, error, request, cancel };
};// 取消
const cancel = () => {if (pollingIntervalTimer.current) {clearTimeout(pollingIntervalTimer.current);pollingIntervalTimer.current = null;status.current && (status.current = false);}
};export default useRequest;
使用
const [mountLoading, setMountLoading] = useState<boolean>(false);useEffect(() => {setMountLoading(true);
}, [2000])const { data, loading, error, request, cancel } = useRequest(queryCompensatoryOrderSituation,{initialData: {compensatoryId,},pollingInterval: 1000,ready: mountLoading,onSuccess: (res) => {console.log('success request!', res);},},
);
依赖请求的思路就是在hook中加入一个ready字段,也是在基于manual一层的限制后又加了一层,来判断是否在hook加载时是否做默认请求,而当option中的ready更新(为true)时,hook自动更新从而发起请求。
常用于页面中A请求完成后执行B请求,B请求的ready字段依赖于A请求的data/loading字段。
防抖与节流
防抖和节流的实现比较简单,依赖于lodash库,包装了一下request函数的请求内容。
代码如下:
useRequest.ts
interface UseRequestOptionsProps {/** 手动开启*/manual?: boolean;/** 请求参数*/initialData?: object;/** 轮询*/pollingInterval?: number | null;/** 准备,用于依赖请求*/ready?: boolean;/** 防抖*/debounceInterval?: number;/** 节流*/throttleInterval?: number;/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const status = useRef<boolean>(false);const pollingIntervalTimer = useRef<NodeJS.Timer | null>(null);const {manual,initialData,pollingInterval,ready = true,debounceInterval,throttleIntervalonSuccess,} = options;useEffect(() => {setLoading(true);setError(null);setData(null);!manual && ready && request();}, [manual, ready]);// 请求const request = () => {if (debounceInterval) {lodash.debounce(requestDoing, debounceInterval)();} else if (throttleInterval) {lodash.throttle(requestDoing, throttleInterval)();} else {requestDoing();}
};// useRequest业务逻辑
const requestDoing = async () => {try {!status.current && (status.current = true);if (pollingInterval && status.current) {pollingIntervalTimer.current = setTimeout(() => {status.current && request();}, pollingInterval);}const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}
};// 取消
const cancel = () => {if (pollingIntervalTimer.current) {clearTimeout(pollingIntervalTimer.current);pollingIntervalTimer.current = null;status.current && (status.current = false);}
};export default useRequest;
使用
const { data, loading, error, request, cancel } = useRequest(queryCompensatoryOrderSituation,{manual: true,initialData: {compensatoryId,},debounceInterval: 1000, // 防抖throttleInterval: 1000, // 节流onSuccess: (res) => {console.log('success request!', res);},},
);for(let i = 0; i < 10000; i++) {request();
}
在hook中,通过lodash.debounce/lodash.throttle来包装request函数主体,通过option中的判断来执行对应的包装体函数。
缓存与依赖更新
改造后的代码(最终代码)如下:
useRequest.ts
import {useState,useEffect,useRef,SetStateAction,useCallback,
} from 'react';
import lodash from 'lodash';interface UseRequestOptionsProps {/** 手动开启*/manual?: boolean;/** 请求参数*/initialData?: object;/** 轮询*/pollingInterval?: number | null;/** 准备,用于依赖请求*/ready?: boolean;/** 防抖*/debounceInterval?: number;/** 节流*/throttleInterval?: number;/** 延迟loading为true的时间*/loadingDelay?: number;/** 依赖*/refreshDeps?: any[];/** 请求成功回调*/onSuccess?: (res: any) => void;
}const useRequest = (requestFn: (initialData?: object | string | [],) => Promise<SetStateAction<any>>,options: UseRequestOptionsProps,
) => {const [data, setData] = useState<SetStateAction<any>>(null);const [loading, setLoading] = useState<boolean>(false);const [error, setError] = useState<string | null>(null);const status = useRef<boolean>(false);const pollingIntervalTimer = useRef<NodeJS.Timer | null>(null);const {manual,initialData,pollingInterval,ready = true,debounceInterval,throttleInterval,loadingDelay,refreshDeps,onSuccess,} = options;useEffect(() => {if (loadingDelay) {setTimeout(() => {status && setLoading(true);}, loadingDelay);}setError(null);setData(null);// 手动触发request!manual && ready && request();}, [manual, ready, ...(Array.isArray(refreshDeps) ? refreshDeps : [])]);// 请求const request = () => {if (debounceInterval) {lodash.debounce(requestDoing, debounceInterval)();} else if (throttleInterval) {lodash.throttle(requestDoing, throttleInterval)();} else {requestDoing();}};// useRequest业务逻辑const requestDoing = async () => {try {!status.current && (status.current = true);if (pollingInterval && status.current) {pollingIntervalTimer.current = setTimeout(() => {status.current && request();}, pollingInterval);}const res = await requestFn(initialData);setData(res);// 请求成功响应回调onSuccess && onSuccess(res);} catch (err) {err && setError(JSON.stringify(err));} finally {setLoading(false);}};// 取消const cancel = () => {if (pollingIntervalTimer.current) {clearTimeout(pollingIntervalTimer.current);pollingIntervalTimer.current = null;status.current && (status.current = false);}};// 缓存const cachedFetchData = useCallback(() => data, [data]);return { data, loading, error, request, cancel, cachedFetchData };
};export default useRequest;
使用
const [mountLoading, setMountLoading] = useState<boolean>(false);
const [updateLoading, setUpdateLoading] = useState<boolean>(false);setTimeout(() => {setMountLoading(true);
}, 1000);setTimeout(() => {setUpdateLoading(true);
}, 2000);const { data, loading, error, request, cancel, cachedFetchData } = useRequest(queryCompensatoryOrderSituation,{manual: true,initialData: {compensatoryId,},debounceInterval: 1000, // 防抖throttleInterval: 1000, // 节流refreshDeps: [mountLoading, updateLoading],onSuccess: (res) => {console.log('success request!', res);},},
);
缓存的主体思路是在useRequest中拿到第一次数据后通过useCallback来透出data依赖来保存,同时向外暴露一个cachedFetchData来过渡data从null到请求到接口数据的过程。
依赖更新的思路则是在页面中给useRequest一系列依赖状态一并加入在hook的请求副作用中,监听到页面中依赖改变,则重新请求,具体实现则是refreshDeps参数。
结尾
花了一上午时间,一个简易版本的useRequest实现了,也是通过实现学习到了一些请求思路,在业务复杂的场景下也是很需要这类请求工具来让开发者的注意力从请求处理转移集中在业务逻辑中。
相关文章:
手写中实现并学习ahooks——useRequest
前言 最近业务没有之前紧张了,也是消失了一段时间,也总结了一些之前业务上的问题。 和同事沟通也是发现普通的async await 封装api在复杂业务场景下针对于请求的业务逻辑比较多,也是推荐我去学习一波ahooks,由于问题起源于请求…...
[手写OS]动手实现一个OS 之 准备工作以及引导扇区
[手写OS]动手实现一个OS之第一步-环境以及引导扇区 环境准备 一台可用计算机(linux我不知道,我用的Windows)汇编编译器NASM一个方便的软盘读写工具VirtualBox 汇编编译器NASM 官网地址:https://www.nasm.us/pub/nasm/snapshot…...
JVM实战OutOfMemoryError异常
目录 Java堆溢出 常见原因: 虚拟机栈和本地方法栈溢出 实验1:虚拟机栈和本地方法栈测试(作为第1点测试程序) 实验2:(作为第1点测试程序) 运行时常量池和方法区溢出 运行时常量池内存溢出 …...
C++虚函数操作指南
1 什么是虚函数?1.1 虚函数的使用规则1.2 用 C 运行虚函数的示例1.3 协变式返回类型2 在 C 中使用虚函数的优点2.1 代码更为灵活、更为通用2.2 代码可复用2.3 契约式设计3 虚函数的局限性3.1 性能3.2 设计问题3.3 调试,容易出错4 虚函数的替代方案4.1 仅…...
Mybatis-Plus分页插件
引言:MyBatis Plus自带分页插件,只要简单的配置即可实现分页功能 1.添加Configuration配置类 Configuration MapperScan("com.atguigu.mybatisplus.mapper") //可以将主类中的注解移到此处public class MybatisPlusConfig {Beanpublic Mybatis…...
Selenium Webdriver options的实用参数设置
1、关闭Chrome浏览器受自动控制的提示 options.add_experimental_option(useAutomationExtension, False) options.add_experimental_option(excludeSwitches, [enable-automation])2、关闭是否保存密码的弹窗 options.add_experimental_option("prefs", { "c…...
代码随想录算法训练营第七天|454.四数相加II 、 383. 赎金信 、 15. 三数之和 、18. 四数之和
454.四数相加II 454.四数相加II介绍给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:思路因为是存放在数组里不同位置的元素,因此不需要考虑去重的操作,而…...
详解抓包原理以及抓包工具whistle的用法
什么是抓包? 分析网络问题业务分析分析网络信息流通量网络大数据金融风险控制探测企图入侵网络的攻击探测由内部和外部的用户滥用网络资源探测网络入侵后的影响监测链接互联网宽频流量监测网络使用流量(包括内部用户,外部用户和系统)监测互联网和用户电脑的安全状…...
【C++】反向迭代器
文章目录一、什么是反向迭代器二、STL 源码中反向迭代器的实现三、reverse_iterator 的模拟实现四、vector 和 list 反向迭代器的实现一、什么是反向迭代器 C 中一共有四种迭代器 – iterator、const_iterator、reverse_iterator 以及 const_reverse_iterator,其中…...
(蓝桥真题)扫描游戏(计算几何+线段树二分)
题目链接:P8777 [蓝桥杯 2022 省 A] 扫描游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 样例输入: 5 2 0 1 1 0 3 2 4 3 5 6 8 1 -51 -33 2 样例输出: 1 1 3 4 -1 分析:先考虑如何对物件进行排序,首先&…...
面试官:什么是双亲委派模型?如何打破它?
本文已经收录进 JavaGuide(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。) 参加过校招面试的同学,应该对这个问题不陌生。一般提问 JVM 知识点的时候,就会顺带问你双亲委派模型(别扭的翻译。。。)。 就算是不准备面试,学习双亲委派模型对于我…...
自建服务器系列- DDNS配置
1、环境说明 光猫桥接路由器拔号的模式 2、DDNS是什么 对于DHCP方式获得的IP,无论对于局域网内来说,还是外网来说,都会有使得IP地址每隔一段时间变化一次,如果想要通过恒定不变的地址访问主机,就需要动态域名解析。…...
vue中使用axios简单封装用法,axios报错the request was rejected because no multipart boundar
在这里插入代码片## 创建实例 //这个写法作为我错误的记录,可以不看暂时 transformRequest: [(data: any) > {if (!data) {data {}}return qs.stringify(data)}]在我的项目里面,初始化配置里面进行handers的修改,例如:例如将…...
Leetcode.1220 统计元音字母序列的数目
题目链接 Leetcode.1220 统计元音字母序列的数目 Rating : 1730 题目描述 给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n的字符串: 字符串中的每个字符都应当是小写元音字母(a, e, i, o, u)…...
深入元空间
元空间是干嘛的?元空间存储的是类的相关信息,就是类的运行时表达。包括:Class文件类的结构和方法常量注解代码优化JDK1.8分界在1.8版本之前,类的meta信息、类变量、字符串常量池都存储在永久代。1.8版本以后,类变量、实…...
前端技术和框架
一、各种技术概述 1.HTML 🧨HTML中文称为超文本标记语言,从语义上来说,它只是一种是一种标识性的语言,并不是一种编程语言。 <p>这是一段话</p>通过这个标签可以表示文本的一个段落。而且其中还有还有图片标签、视…...
02从零开始学Java之Java到底是个啥?
博主简介我是壹壹哥(孙玉昌),十年软件开发授课经验,CSDN博客专家、阿里云专家博主、掘金优秀创作者、infoQ专家博主;关注壹壹哥(孙玉昌),带你玩转Java,轻松实现从入门到放弃,哦不,到熟悉&#x…...
KEIL5中头文件路劲包含问题
方式1:1.Keil中添加头文件相对路劲的方法在c/c配置中添加路劲,最终是将添加的绝对路径转化为相对路径;注意:相对路径的当前位置指.uvproj文件所在位置在C/C配置中的include paths”中添加工程所用的所有头文件的路径;2…...
机智云目前我用过最便捷的物联网快速开发方案
GE211 MINI DTU上手来看,是一款尺寸比较小巧的模块,适合放置在几乎所有白色家电中,通过ph2.0端子(注意不要买错)引出了5v、gnd、tx、rx。可以说是非常方便了。下面正式开始我们的接入流程:首先注册一个机智…...
MySQL基础篇1
第1章 数据库介绍 1.1 数据库概述 什么是数据库? 数据库就是存储数据的仓库,其本质是一个文件系统,数据按照特定的格式将数据存储起来,用户可以对数据库中的数据进行增加,修改,删除及查询操作。 数据库分两…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
STM32标准库-DMA直接存储器存取
文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA(Direct Memory Access)直接存储器存取 DMA可以提供外设…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...
Psychopy音频的使用
Psychopy音频的使用 本文主要解决以下问题: 指定音频引擎与设备;播放音频文件 本文所使用的环境: Python3.10 numpy2.2.6 psychopy2025.1.1 psychtoolbox3.0.19.14 一、音频配置 Psychopy文档链接为Sound - for audio playback — Psy…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...
使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...
Spring AI Chat Memory 实战指南:Local 与 JDBC 存储集成
一个面向 Java 开发者的 Sring-Ai 示例工程项目,该项目是一个 Spring AI 快速入门的样例工程项目,旨在通过一些小的案例展示 Spring AI 框架的核心功能和使用方法。 项目采用模块化设计,每个模块都专注于特定的功能领域,便于学习和…...
