學習ahooks useRequest並實現手寫
前言
最近業務沒有之前緊張瞭,也是消失瞭一段時間,也總結瞭一些之前業務上的問題。
和同事溝通也是發現普通的async
+ await
+ 封裝api
在復雜業務場景下針對於請求的業務邏輯比較多,也是推薦我去學習一波ahooks,由於問題起源於請求,因此作者也是直接從 useRequest
開始看起。
附ahooks useRequest
鏈接:
ahooks-v2.js.org/zh-CN/hooks…
實現
話不多說,手寫直接開始,參考幾個比較常用的 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, throttleInterval onSuccess, } = 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並實現手寫的詳細內容,更多關於ahooks useRequest手寫的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- ahooks useRequest源碼精讀解析
- 列表頁常見hook封裝實例
- vant-list上拉加載onload事件觸發多次問題及解決
- React hooks useState異步問題及解決
- 詳解Vue 自定義hook 函數