大家會發現,自從 React v16.8 推出了 Hooks API,前端框架圈并開啟了新的邏輯復用的時代,從此無需在意 HOC 的無限套娃導致性能差的問題,同時也解決了 mixin 的可閱讀性差的問題。這里也有對于 React 最大的變化是函數式組件可以有自己的狀態,扁平化的邏輯組織方式,更加友好地支持 TS 類型聲明。
在運用Hooks的時候,除了 React 官方提供的,同時也支持我們能根據自己的業務場景自定義 Hooks,還有一些通用的 Hooks,例如用于請求的useRequest,用于定時器的useTimeout,用于節流的useThrottle等。于是出現了大量的 Hooks 庫,ahooks是其中比較受歡迎的 Hooks 庫之一,其提供了大量的 Hooks,基本滿足了大多數場景的需求。又是國人開發,中文文檔友好,在我們團隊的一些項目中就使用了 ahooks。
其中最常用的 hooks 就是useRequest,用于從后端請求數據的業務場景,除了簡單的數據請求,它還支持:
輪詢
防抖和節流
錯誤重試
SWR(stale-while-revalidate)
緩存
等功能,這樣看起來是不是基本上滿足了我們請求后端數據需要考慮的大多數場景,其中還有 loading-delay、頁面 foucs 重新刷新數據等這些功能,就個人看法上面的功能才是使用比較頻繁的功能點。
一個 Hooks 實現這么多功能,不禁感嘆它的強大,所以本文就從源碼的角度帶大家了解 useRequest 的實現。
架構圖
下面是關于了解其模塊設計,對于一個功能復雜的 API,如果不使用合適的架構和方式組織代碼,其擴展性和可維護性肯定比較差。功能點實現和核心代碼混在一起,閱讀代碼的人也無從下手,也帶來更大的測試難度。雖然 useRequest 只是一個 Hook,但是實際上其設計還是有清晰的架構,我們來看看 useRequest 的架構圖:
將 useRequest 的模塊劃分為三大塊:Core、Plugins、utils,然后 useRequest 將這些模塊組合在一起實現核心功能。
先看插件部分,看到每個插件的命名,如果了解 useRequest 的功能就會發現,基本上每個功能點對應一個插件。這也是 useRequest 設計比較巧妙的一點,通過插件化機制降低了每個功能之間的耦合度,也降低了其本身的復雜度。這些點我們在分析具體的源碼的時候會再詳細介紹。
另外一部分核心的代碼我將其歸類為 Core(在 useRequest 的源碼中沒有這個名詞),主要實現了一個 Fetch 類,這個類是 useRequest 的插件化機制實現和其它功能的核心實現。
下面我們深入源碼,看下其實現原理。
源碼解析
先看 Core 部分的源碼,主要是 Fetch 這個類的實現。
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 的使用的地方,我們再說。
onBefore
接下來 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 鉤子我們是可以重寫傳入的獲取數據的方法,因為最后執行的是 onRequest 回調返回的servicePromise。
拿到最后執行的請求數據方法,就開始發起請求。在這里發現了前面的 currentCount 的使用,它會去對比當前最新的 count 和執行這個方法時定義的 currentCount 是否相等,如果不相等,則會做類似于取消請求的處理。這里大概知道 count 的作用類似于一個”鎖“的作用,我的理解是,如果在執行這些代碼過程有產生一些比這里優先級更高的處理邏輯或者請求操作,是需要 cancel 掉這次的請求,以最新的請求為準。當然,最后還是要看哪些地方可能會修改 count。
onSuccess
執行完請求后,如果請求成功,則拿到請求返回的數據,更新到 state,執行用戶傳入的成功回調和各插件的成功回調鉤子。
onFinally
成功之后,執行 onFinally 鉤子,這里也很嚴謹,也會比較 count 的值,確保一致之后,才會執行各插件的回調鉤子,預發一些”競態“情況的發生。
onError
如果請求失敗,就會進入到 catch 分支,執行一些處理錯誤的邏輯,更新 error 信息到 state 中。同樣這里也會有 count 的對比,然后執行 onError 的回調。執行完 onError 也會同樣執行 onFinally 的回調,因為一個請求要么成功,要么失敗,都會需要執行最后的 onFinally 回調。
其它 API
其它的例如 run、cancel、refresh 等 API,實際上調用的是runPluginHandler和runAsyncAPI,例如 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 不管有多少插件,只負責在合適的時間點調用插件鉤子,做到了完全的解耦。
plugins
其實看完了 Fetch,還沒看插件,你腦子里就大概知道怎么去實現一個插件。因為插件比較多,限于篇幅原因,這里就以 usePollingPlugin 和 useRetryPlugin 兩個插件為例,進行詳細的源碼介紹。
usePollingPlugin
首先需要清楚一點每個插件實際也是一個 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 等的時候,需要先停止掉之前的輪詢。
useRetryPlugin
假設讓你去設計一個 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。首先在 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 是支持用戶自定義插件的,這又突出了插件化設計的必要性。除了能降低本身自己的功能之間的復雜度,也能提供更多的靈活度給到用戶,如果你覺得功能不夠,實現自定義插件吧。
對自定義 hook 的思考
面向對象編程里面有一個原則叫職責單一原則, 我個人理解它的含義是我們在設計一個類或者一個方法時,它的職責應該盡量單一。如果一個類的抽象不在一個層次,那么這個類注定會越來越膨脹,難以維護。一個方法職責越單一,它的復用性就可能越高,可測試性也越好。
其實我們在設計一個 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 內部。
總結
我們現在降的就是從 Fetch 類的實現和 plugins 的設計詳細解析了 useRequest 的源碼。
useRequest 核心源碼主要在 Fetch 類的實現中,主要是通過巧妙的將請求劃分為各個階段的設計,之后將豐富的功能交給每個插件去實現,解耦功能之間的關系,降低本身維護的復雜度,提高可測試性;
useRequest 雖然只是一個代碼千行左右的 Hook,但是通過插件化機制,使得各個功能之間完全解耦,提高了代碼的可維護性和可測試性,同時也提供了用戶自定義插件的能力;
職責單一的原則在任何場景下引用都不會過時,我們在設計一些 Hook 的時候應該也要考慮單一原則。但是在設計一些跨多項目通用的 Hook,應該朝著深模塊的角度設計,提供簡單的接口,把復雜度隱藏在模塊內部。
知識點都已講述了,只看每個人自己的理解。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/128265.html
起因 社會在不斷的向前,技術也在不斷的完善進步。從 React Hooks 正式發布到現在,越來越多的項目正在使用 Function Component 替代 Class Component,Hooks 這一新特性也逐漸被廣泛的使用。 這樣的解析是不是很熟悉,在日常中時常都有用到,但也有一個可以解決這樣重復的就是對數據請求的邏輯處理,對防抖節流的邏輯處理等。 另一方面,由于 Hoo...
我們今天來講講關于ahooks 源碼,我們目標主要有以下幾點: 深入了解 React hooks。 明白如何抽象自定義 hooks,且可以構建屬于自己的 React hooks 工具庫。 小建議:培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。 列表頁常見元素 后臺管理系統中常見典型列表頁包括篩選表單項、Table表格、Pagination分頁這三部分。 針對使用 Ant...
我們講下 ahooks 的核心 hook —— useRequest。 useRequest 簡介 根據官方文檔的介紹,useRequest 是一個強大的異步數據管理的 Hooks,React 項目中的網絡請求場景使用 useRequest ,這就可以。 useRequest通過插件式組織代碼,核心代碼極其簡單,并且可以很方便的擴展出更高級的功能。目前已有能力包括: 自動請求/手動請求 ...
這是講 ahooks 源碼的第一篇文章,簡要就是以下幾點: 加深對 React hooks 的理解。 學習如何抽象自定義 hooks。構建屬于自己的 React hooks 工具庫。 培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。 注:本系列對 ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見詳情。 第一篇主要介紹 a...
我們講述的是關于 ahooks 源碼系列文章的第七篇,總結主要講述下面幾點: 鞏固 React hooks 的理解。 學習如何抽象自定義 hooks。構建屬于自己的 React hooks 工具庫。 培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。 注:本系列對 ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見詳情。 ...
閱讀 561·2023-03-27 18:33
閱讀 750·2023-03-26 17:27
閱讀 647·2023-03-26 17:14
閱讀 603·2023-03-17 21:13
閱讀 537·2023-03-17 08:28
閱讀 1823·2023-02-27 22:32
閱讀 1315·2023-02-27 22:27
閱讀 2199·2023-01-20 08:28