您好,登錄后才能下訂單哦!
這篇“ahooks useRequest源碼分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“ahooks useRequest源碼分析”文章吧。
自從 React v16.8 推出了 Hooks API,前端框架圈并開啟了新的邏輯復用的時代,不再需要在意 HOC 的無限套娃導致性能差的問題,也解決了 mixin 的可閱讀性差的問題。當然對于 React 最大的變化是函數式組件可以有自己的狀態,扁平化的邏輯組織方式,更加友好地支持 TS 類型聲明。
除了 React 官方提供的一些 Hooks,也支持我們能根據自己的業務場景自定義 Hooks,還有一些通用的 Hooks,例如用于請求的 useRequest
,用于定時器的 useTimeout
,用于節流的 useThrottle
等。于是出現了大量的 Hooks 庫,ahooks 是其中比較受歡迎的 Hooks 庫之一,其提供了大量的 Hooks,基本滿足了大多數場景的需求。
其中最常用的 hooks 就是 useRequest
,用于從后端請求數據的業務場景,除了簡單的數據請求,它還支持:
輪詢
防抖和節流
錯誤重試
SWR(stale-while-revalidate)
緩存
等功能,基本上滿足了我們請求后端數據需要考慮的大多數場景,當然還有 loading-delay、頁面 foucs 重新刷新數據等這些功能,但是個人理解上面列的功能才是使用比較頻繁的功能點。
我們從一張圖開始了解其模塊設計,對于一個功能復雜的 API,如果不使用合適的架構和方式組織代碼,其擴展性和可維護性肯定比較差。功能點實現和核心代碼混在一起,閱讀代碼的人也無從下手,也帶來更大的測試難度。雖然 useRequest 只是一個 Hook,但是實際上其設計還是有清晰的架構,我們來看看 useRequest 的架構圖:
我把 useRequest 的模塊劃分為三大塊:Core、Plugins、utils,然后 useRequest 將這些模塊組合在一起實現核心功能。
先看插件部分,看到每個插件的命名,如果了解 useRequest 的功能就會發現,基本上每個功能點對應一個插件。這也是 useRequest 設計比較巧妙的一點,通過插件化機制降低了每個功能之間的耦合度,也降低了其本身的復雜度。這些點我們在分析具體的源碼的時候會再詳細介紹。
另外一部分核心的代碼我將其歸類為 Core(在 useRequest 的源碼中沒有這個名詞),主要實現了一個 Fetch 類,這個類是 useRequest 的插件化機制實現和其它功能的核心實現。
下面我們深入源碼,看下其實現原理。
先看 Core 部分的源碼,主要是 Fetch 這個類的實現。
先貼代碼:
export default class Fetch<TData, TParams extends any[]> { pluginImpls: PluginReturn<TData, TParams>[]; count: number = 0; state: FetchState<TData, TParams> = { loading: false, params: undefined, data: undefined, error: undefined, }; constructor( public serviceRef: MutableRefObject<Service<TData, TParams>>, public options: Options<TData, TParams>, public subscribe: Subscribe, public initState: Partial<FetchState<TData, TParams>> = {}, ) { this.state = { ...this.state, loading: !options.manual, ...initState, }; } setState(s: Partial<FetchState<TData, TParams>> = {}) { // 省略一些代碼 } runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { // 省略一些代碼 } async runAsync(...params: TParams): Promise<TData> { // 省略一些代碼 } run(...params: TParams) { // 省略一些代碼 } cancel() { // 省略一些代碼 } refresh() { // 省略一些代碼 } refreshAsync() { // 省略一些代碼 } mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { // 省略一些代碼 } }
Fetch 類 API 的設計還是比較簡潔的,而且也不是特別多,實際上有些 API 就是直接從 useRequest 暴露給外部用戶使用的,比如 run、runAsync、cancel、refresh、refreshAsync、mutate 等。像 runPluginHandler、setState 等 API 主要是給內部用的 API,不過它也沒有做區分,從封裝的角度上來說,這一點個人感覺設計得不夠好。
重點關注下幾個 Fetch 類的屬性,一個是 state,它的類型是 FetchState<TData, TParams>
,一個是 pluginImpls,它是 PluginReturn<TData, TParams>
數組,實際上這個屬性就用來存所有插件執行后返回的結果。還有一個 count 屬性,是 number
類型,不看具體源碼,完全不知道這個屬性是做什么用的。這點也是 useRequest 開發者做得感覺不是很好的地方,很少有注釋,純靠閱讀者深入到源碼,去看使用的地方,才能知道一些方法和屬性的作用。
那我們先來看下 FetchState<TData, TParams>
的定義,它定義在 src/type.ts 里面:
export interface FetchState<TData, TParams extends any[]> { loading: boolean; params?: TParams; data?: TData; error?: Error; }
它的定義還是比較簡單,看起來是存一個請求結果的上下文信息,這些信息其實都是需要暴露給外部用戶的,例如 loading
、data
、errors
等不就是我們使用 useRequest 經常需要拿到的數據信息:
const { data, error, loading } = useRequest(service);
而對應的 Fetch 封裝了 setState API,實際上就是用來更新 state 的數據:
setState(s: Partial<FetchState<TData, TParams>> = {}) { this.state = { ...this.state, ...s, }; // ? 未知 this.subscribe(); }
除了更新 state,這里還調用了一個 subscribe 方法,這是初始化 Fetch 類的時候傳進來的一個參數,它的類型是 Subscribe
,等后面將到調用的地方再看這個方法是怎么實現的,以及它的作用。
再看下 PluginReturn<TData, TParams>
的類型定義:
export interface PluginReturn<TData, TParams extends any[]> { onBefore?: (params: TParams) => | ({ stopNow?: boolean; returnNow?: boolean; } & Partial<FetchState<TData, TParams>>) | void; onRequest?: ( service: Service<TData, TParams>, params: TParams, ) => { servicePromise?: Promise<TData>; }; onSuccess?: (data: TData, params: TParams) => void; onError?: (e: Error, params: TParams) => void; onFinally?: (params: TParams, data?: TData, e?: Error) => void; onCancel?: () => void; onMutate?: (data: TData) => void; }
實際上都是一些回調鉤子,從名字對應上來看,對應了請求的各個階段,除了 onMutate
是其內部擴展的一個鉤子。
也就是說 pluginImpls 里面存的是一堆含有各個鉤子函數的對象集合,如果技術敏銳的同學,可能很容易就想到發布訂閱模式,這不就是存了一系列的 subscribe 回調,這不過這是一個回調的集合,里面有各種不同請求階段的回調。那么到底是不是這樣,我們繼續往下看。
要搞清楚 Fetch 的運作方式,我們需要看兩個核心 API 的實現:runPluginHandler
和 runAsync
,其它所有的 API 實際上都在調用這兩個 API,然后做一些額外的特殊邏輯處理。
先看 runPluginHandler
:
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { // @ts-ignore const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); return Object.assign({}, ...r); }
這個方法實現還是比較簡單,只有兩行代碼。跟我們之前猜測的大致差不多,這個方法就是接收一個 event 參數,它的類型就是 keyof PluginReturn<TData, TParams>
,也就是:onBefore | onRequest | onSuccess | onError | onFinally | onCancel | onMutate
的聯合類型,以及其它額外的參數,然后從 pluginImpls 中找出所有對應的 event 回調鉤子函數,然后執行回調函數,拿到結果并返回。
再看 runAsync
的實現:
async runAsync(...params: TParams): Promise<TData> { this.count += 1; const currentCount = this.count; const { stopNow = false, returnNow = false, ...state } = this.runPluginHandler('onBefore', params); // stop request if (stopNow) { return new Promise(() => {}); } this.setState({ loading: true, params, ...state, }); // return now if (returnNow) { return Promise.resolve(state.data); } this.options.onBefore?.(params); try { // replace service let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params); if (!servicePromise) { servicePromise = this.serviceRef.current(...params); } const res = await servicePromise; if (currentCount !== this.count) { // prevent run.then when request is canceled return new Promise(() => {}); } // const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res; this.setState({ data: res, error: undefined, loading: false, }); this.options.onSuccess?.(res, params); this.runPluginHandler('onSuccess', res, params); this.options.onFinally?.(params, res, undefined); if (currentCount === this.count) { this.runPluginHandler('onFinally', params, res, undefined); } return res; } catch (error) { if (currentCount !== this.count) { // prevent run.then when request is canceled return new Promise(() => {}); } this.setState({ error, loading: false, }); this.options.onError?.(error, params); this.runPluginHandler('onError', error, params); this.options.onFinally?.(params, undefined, error); if (currentCount === this.count) { this.runPluginHandler('onFinally', params, undefined, error); } throw error; } }
看著代碼挺多的,其實看下來很好理解。 這個函數實際上做的事就是調用我們傳入的獲取數據的方法,然后拿到成功或者失敗的結果,進行一系列的數據處理,然后更新到 state,執行插件的各回調鉤子,還有就是我們通過 options 傳入的回調函數。
可能直接用文字直接描述比較抽象,下面我們分請求階段分析代碼。
首先前兩行是對 count 屬性的累加處理,之前我們不知道這個屬性的作用,看到這里可能猜測大概是跟請求相關的,后面看到 currentCount 的使用的地方,我們再說。
接下來 5~27 行實際上是對 onBefore 回調鉤子的執行,然后拿到結果做的一些邏輯處理。這里調用的就是 runPluginHandler 方法,傳入的參數是 onBefore 和外部用戶定義的 params 參數。然后執行完所有的 onBefore 鉤子函數,拿到最后的結果,如果 stopNow 的 flag 是 true,則直接返回沒有結果的 Promise。看注釋,我們知道這里實際上做的是取消請求的處理,當我們在 onBefore 的鉤子里實現了取消的邏輯,符合條件后并會真正的阻斷請求。
如果沒有取消,然后接著更新 state 數據,如果立即返回的 returnNow flag 為 true,則立馬將更新后的 state 返回,否則執行用戶傳入的 options 中的 onBefore 回調,也就是說在調用 useRequest 的時候,我們可以通過 options 參數傳入 onBefore 函數,進行請求之前的一些邏輯處理。
接下來后面的代碼就是真正執行請求數據的方法了,這里就會執行所有的 onRequest 鉤子。實際上,通過 onRequest 鉤子我們是可以重寫傳入的獲取數據的方法,因為最后執行的是 onRequest 回調返回的 servicePromise
。
拿到最后執行的請求數據方法,就開始發起請求。在這里發現了前面的 currentCount 的使用,它會去對比當前最新的 count 和執行這個方法時定義的 currentCount 是否相等,如果不相等,則會做類似于取消請求的處理。這里大概知道 count 的作用類似于一個”鎖“的作用,我的理解是,如果在執行這些代碼過程有產生一些比這里優先級更高的處理邏輯或者請求操作,是需要 cancel 掉這次的請求,以最新的請求為準。當然,最后還是要看哪些地方可能會修改 count。
執行完請求后,如果請求成功,則拿到請求返回的數據,更新到 state,執行用戶傳入的成功回調和各插件的成功回調鉤子。
成功之后,執行 onFinally 鉤子,這里也很嚴謹,也會比較 count 的值,確保一致之后,才會執行各插件的回調鉤子,預發一些”競態“情況的發生。
如果請求失敗,就會進入到 catch 分支,執行一些處理錯誤的邏輯,更新 error 信息到 state 中。同樣這里也會有 count 的對比,然后執行 onError 的回調。執行完 onError 也會同樣執行 onFinally 的回調,因為一個請求要么成功,要么失敗,都會需要執行最后的 onFinally 回調。
其它的例如 run、cancel、refresh 等 API,實際上調用的是 runPluginHandler
和 runAsync
API,例如 run:
run(...params: TParams) { this.runAsync(...params).catch((error) => { if (!this.options.onError) { console.error(error); } }); }
代碼很容易看懂,就不過多介紹。
我們來看看 cancel 的實現:
cancel() { this.count += 1; this.setState({ loading: false, }); this.runPluginHandler('onCancel'); }
最后的 runPluginHandler 調用我們已經很清楚它的作用了,這里值得注意的是對 count 的修改。前面我們提到每次 runAsync 一些核心階段會判斷 count 是否和 currentCount 能對得上,看到這里我們就徹底明白了 count 的作用了。實際上在我們執行了 run 的操作,如果在本次 runAsync 方法執行過程中,我們就調用了 cancel 方法,那么無論是在請求發起前還是后,都會把本次執行當做 cancel 處理,返回空的數據。也就是說,這個 count 就是為了實現請求取消功能的一個標識。
看完了 runAsync
的實現,實際上就代表我們看完了 Fetch 的核心邏輯。從一個請求的生命周期角度來看,其實它的實現就很容易理解,主要做兩件事:
執行各階段的鉤子回調;
更新數據到 state。
這歸功于 useRequest 的巧妙設計,我們看這部分源碼,只要看懂了類型和兩個核心的方法,都不用關心具體每個插件的實現。它將每個功能點的復雜度和核心的邏輯通過插件機制隔離開來,從而每個插件只需要按一定的契約實現好自己的功能就行,然后 Fetch 不管有多少插件,只負責在合適的時間點調用插件鉤子,做到了完全的解耦。
其實看完了 Fetch,還沒看插件,你腦子里就大概知道怎么去實現一個插件。因為插件比較多,限于篇幅原因,這里就以 usePollingPlugin 和 useRetryPlugin 兩個插件為例,進行詳細的源碼介紹。
首先需要清楚一點每個插件實際也是一個 Hook,所以在它內部可以使用任何 Hook 的功能或者調用其它 Hook。先看 usePollingPlugin:
const usePollingPlugin: Plugin<any, any[]> = ( fetchInstance, { pollingInterval, pollingWhenHidden = true }, ) => { const timerRef = useRef<NodeJS.Timeout>(); const unsubscribeRef = useRef<() => void>(); const stopPolling = () => { if (timerRef.current) { clearTimeout(timerRef.current); } unsubscribeRef.current?.(); }; useUpdateEffect(() => { if (!pollingInterval) { stopPolling(); } }, [pollingInterval]); if (!pollingInterval) { return {}; } return { onBefore: () => { stopPolling(); }, onFinally: () => { // if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible if (!pollingWhenHidden && !isDocumentVisible()) { unsubscribeRef.current = subscribeReVisible(() => { fetchInstance.refresh(); }); return; } timerRef.current = setTimeout(() => { fetchInstance.refresh(); }, pollingInterval); }, onCancel: () => { stopPolling(); }, }; };
它接受兩個參數,一個是 fetchInstance,也就是前面提到的 Fetch 實例,第二個參數是 options,支持傳入 pollingInterval、pollingWhenHidden 兩個屬性。這兩個屬性從命名上比較容易理解,一個就是輪詢的時間間隔,另外一個猜測應該是可以在某種場景下通過設置這個 flag 停止輪詢。在真實的場景中,確實有比如要求用戶在切換到其它 tab 頁時停止輪詢等這樣的需求。所以這個配置,還比較好理解。
而每個插件的作用就是在請求的各個階段進行定制化的邏輯處理,以輪詢為例,其最核心的邏輯在于 onFinally 的回調,在每次請求結束后,設置一個 setTimeout,然后按用戶傳入的 pollingInterval 進行定時執行 Fetch 的 refresh 方法。
還有就是停止輪詢的時機,每次用戶主動取消請求,在 onCancel 的回調停止輪詢。如果已經開始了輪詢,在每次新的請求調用的時候先停止上一次的輪詢,避免重復。當然包括,如果組件修改了 pollingInterval 等的時候,需要先停止掉之前的輪詢。
假設讓你去設計一個 retry 的插件,那么你的設計思路是什么了?需要關注的核心邏輯是什么?還是前面那句話: 每個插件的作用就是在請求的各個階段進行定制化的邏輯處理,那如果要實現 retry 肯定你首要關注的是,什么時候才需要 retry?答案顯而易見,那就是請求失敗的時候,也就是需要在 onError 回調實現 retry 的邏輯。考慮得周全一點,你還需要知道 retry 的次數,因為第二次也可能失敗了。當然還有就是 retry 的時間間隔,失敗后多久 retry?這些是外部使用者關心的,所以應該將它們設計成配置項。
分析好了需求,我們看下 retry 插件的實現:
const useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => { const timerRef = useRef<NodeJS.Timeout>(); const countRef = useRef(0); const triggerByRetry = useRef(false); if (!retryCount) { return {}; } return { onBefore: () => { if (!triggerByRetry.current) { countRef.current = 0; } triggerByRetry.current = false; if (timerRef.current) { clearTimeout(timerRef.current); } }, onSuccess: () => { countRef.current = 0; }, onError: () => { countRef.current += 1; if (retryCount === -1 || countRef.current <= retryCount) { // Exponential backoff 指數補償 const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000); timerRef.current = setTimeout(() => { triggerByRetry.current = true; fetchInstance.refresh(); }, timeout); } else { countRef.current = 0; } }, onCancel: () => { countRef.current = 0; if (timerRef.current) { clearTimeout(timerRef.current); } }, }; };
第一個參數跟 usePollingPlugin 的插件一樣,都是接收 Fetch 實例,第二個參數是 options,支持 retryInterval、retryCount 等選型,從命名上看跟我們剛開始分析需求的時候想的差不多。
看代碼,核心的邏輯主要是在 onError 的回調中。首先前面定義了一個 countRef,記錄 retry 的次數。執行了 onError 回調,代表新的一次請求錯誤發生,然后判斷如果 retryCount 為 -1,或者當前 retry 的次數還小于用戶自定義的次數,則通過一個定時器設置下次 retry 的時間,否則將 countRef 重置。
還需要注意的是其它的一些回調的處理,比如當請求成功或者被取消,需要重置 countRef,取消的時候還需要清理可能存在的下一次 retry 的定時器。
這里 onBefore 的邏輯處理怎么理解了?首先這里會有一個 triggerByRetry 的 flag,如果 flag 是 false。則會清空 countRef。然后會將 triggerByRetry 設置為 false,然后清理掉上一次可能存在的 retry 定時器。我個人的理解是這里設置一個 flag 是為了避免如果 useRequest 重新執行,導致請求重新發起,那么在 onBefore 的時候需要做一些重置處理,以防和上一次的 retry 定時器撞車。
其它插件的設計思路是類似的,關鍵是要分析出你需要實現的功能是作用在請求的哪個階段,那么就需要在這個鉤子里實現核心的邏輯處理。然后再考慮其它鉤子的一些重置處理,取消處理等,所以在優秀合理的設計下實現某個功能它的成本是很低的,而且也不需要關心其它插件的邏輯,這樣每個插件也是可以獨立測試的。
分析了核心的兩塊源碼,我們來看下,怎么組裝最后的 useRequest。首先在 useRequest 之前,還有一層抽象叫 useRequestImplement,看下是怎么實現的:
function useRequestImplement<TData, TParams extends any[]>( service: Service<TData, TParams>, options: Options<TData, TParams> = {}, plugins: Plugin<TData, TParams>[] = [], ) { const { manual = false, ...rest } = options; const fetchOptions = { manual, ...rest, }; const serviceRef = useLatest(service); const update = useUpdate(); const fetchInstance = useCreation(() => { const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); return new Fetch<TData, TParams>( serviceRef, fetchOptions, update, Object.assign({}, ...initState), ); }, []); fetchInstance.options = fetchOptions; // run all plugins hooks // 這里為什么可以使用 map 循環去執行每個插件 hooks fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions)); useMount(() => { if (!manual) { // useCachePlugin can set fetchInstance.state.params from cache when init const params = fetchInstance.state.params || options.defaultParams || []; // @ts-ignore fetchInstance.run(...params); } }); useUnmount(() => { fetchInstance.cancel(); }); return { loading: fetchInstance.state.loading, data: fetchInstance.state.data, error: fetchInstance.state.error, params: fetchInstance.state.params || [], cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)), refresh: useMemoizedFn(fetchInstance.refresh.bind(fetchInstance)), refreshAsync: useMemoizedFn(fetchInstance.refreshAsync.bind(fetchInstance)), run: useMemoizedFn(fetchInstance.run.bind(fetchInstance)), runAsync: useMemoizedFn(fetchInstance.runAsync.bind(fetchInstance)), mutate: useMemoizedFn(fetchInstance.mutate.bind(fetchInstance)), } as Result<TData, TParams>; }
前面兩個參數如果使用過 useRequest 的都知道,就是我們通常傳給 useRequest 的參數,一個是請求 api,一個就是 options。這里還多了個插件參數,大概可以知道,內置的一些插件應該會在更上層的地方傳進來,做一些參數初始化的邏輯。
然后通過 useLatest 構造一個 serviceRef,保證能拿到最新的 service。接下來,使用 useUpdate Hook 創建了update 方法,然后再創建 fetchInstance 的時候作為第三個參數傳遞給 Fetch,這里就是我們前面提到過的 subscribe。那我們要看下 useUpdate 做了什么:
const useUpdate = () => { const [, setState] = useState({}); return useCallback(() => setState({}), []); };
原來是個”黑科技“,類似 class 組件的 $forceUpdate API,就是通過 setState,讓組件強行渲染一次。
接著就是使用 useMount,如果發現用戶沒有設置 manual 或者將其設置為 false,立馬會執行一次請求。當組件被銷毀的時候,在 useUnMount 中進行請求的取消。最后返回暴露給用戶的數據和 API。
最后看下 useRequest 的實現:
function useRequest<TData, TParams extends any[]>( service: Service<TData, TParams>, options?: Options<TData, TParams>, plugins?: Plugin<TData, TParams>[], ) { return useRequestImplement<TData, TParams>(service, options, [ ...(plugins || []), useDebouncePlugin, useLoadingDelayPlugin, usePollingPlugin, useRefreshOnWindowFocusPlugin, useThrottlePlugin, useRefreshDeps, useCachePlugin, useRetryPlugin, useReadyPlugin, ] as Plugin<TData, TParams>[]); }
這里就會把內置的插件傳入進去,當然還有用戶自定義的插件。實際上 useRequest 是支持用戶自定義插件的,這又突出了插件化設計的必要性。除了能降低本身自己的功能之間的復雜度,也能提供更多的靈活度給到用戶,如果你覺得功能不夠,實現自定義插件吧。
面向對象編程里面有一個原則叫職責單一原則, 我個人理解它的含義是我們在設計一個類或者一個方法時,它的職責應該盡量單一。如果一個類的抽象不在一個層次,那么這個類注定會越來越膨脹,難以維護。一個方法職責越單一,它的復用性就可能越高,可測試性也越好。
其實我們在設計一個 hooks,也是需要參照這個原則的。Hooks API 出現的一個重大意義,就是解決我們在編寫組件時的邏輯復用問題。沒有 Hooks,之前是使用 HOC、Render props或者 Mixin 等解決邏輯復用的問題,然而每一種方式在大量實踐后都發現有明顯的缺點。所以,我們在自定義一個 Hook 時,總是應該朝著提高復用性的角度出發。
光說太抽象,舉個之前我在業務開發中遇到的一個例子。在一個項目中,我們封裝了一個計算預算的 Hook 叫 useBudgetValidate
,不方便貼所有代碼,下面通過偽代碼列下這個 Hook 做的事:
export default function useBudgetValidate({ id, dailyBudgetType, mode }: Options) { const [dailyBudgetSetting, setDailyBudgetSetting] = useState<BudgetSetting | null>(null); // 從后端獲取某個數據 const { data: adSetCountRes } = useRequest( (campaign: ReactText) => getSomeData({ params: { id } })); // 從后端獲取預算配置 useRequest( () => { return getBudgetSetting(); }, { onSuccess: result => setDailyBudgetSetting(result), }, ); /** * 對于傳入的預算的類型, 返回的預算設置 */ const currentDailyBudgetSetting: DailyBudgetSetting | undefined = useMemo(() => { if (dailyBudgetType === BudgetTypeEnum.AdSet) { return dailyBudgetSetting?.adset; } if (dailyBudgetType === BudgetTypeEnum.Smart) { return dailyBudgetSetting?.smart; } const campaignBudget = dailyBudgetSetting?.campaign; // 這里有大量的計算邏輯,得到最后的 campaignBudget return campaignBudget; }, []); return { currentDailyBudgetSetting, dailyBudgetSetting, }; }
初一看,這個 Hook 沒有太大的問題,不就是從后端獲取數據,然后根據不同的傳參進行預算計算,然后返回預算信息。但是現在有個問題,因為計算預算是項目通用的邏輯。在另外一個頁面也需要這段計算邏輯,但是那個頁面已經從后端其它的接口獲取了預算信息,或者通過其它方式構造了計算預算需要的數據。所以這里的核心矛盾點在于很多頁面依賴這段計算邏輯,但是數據來源是不一致的。將獲取預算配置和其它信息的接口邏輯放在這個 Hook 里面就會導致它的職責不單一,所以沒法很容易在其它場景復用。
重構的思路很簡單,就是將數據請求的邏輯抽離,單獨封裝一個 Hook,或者把職責交給組件去做。這個 Hook 只做一件事,那就是接收配置和其它參數,進行預算計算,將結果返回給外面。
但是對于 useRequest 這樣功能很復雜的 Hook 又怎么理解了?從功能上看,感覺它既做了一般請求數據的功能,又做了輪詢,做了緩存,做了重試,做了。。。反正很多很多的職責。
但是,如果你認真思考,發現這些功能又是依賴請求這個關鍵點,也就是說從這個角度來看,它們的抽象是在同一層次上。而且 useRquest 是一個更加通用的 Hook,它作為一個 package 給大量的用戶使用。如果你是一個使用者,你八成希望它是什么能力都有,你需要的它有,你暫時不需要的,它也幫你想好了。
在 Philosophy of Software Design 一書中提到一個概念叫:深模塊,它的意思是:深模塊是那些既提供了強大功能但又有著簡單接口的模塊。在設計一些模塊或者 API 的時候,比如像 useRequest 這種,那么就要符合這個原則,用戶只需要少量的配置,就能使用各插件帶來的豐富功能。
所以最后,總結下:如果我們在日常業務開發封裝一些 Hook,我們應該盡量保證職責單一,以提高其復用性。如果我們需要設計一個抽象程度很高,然后給多個項目使用的 Hook,那么在設計的時候,應該符合深模塊的特點,接口盡量簡單,又需要滿足各需求場景,將功能復雜度隱藏在 Hook 內部。
以上就是關于“ahooks useRequest源碼分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。