大家會(huì)發(fā)現(xiàn),自從 React v16.8 推出了 Hooks API,前端框架圈并開啟了新的邏輯復(fù)用的時(shí)代,從此無需在意 HOC 的無限套娃導(dǎo)致性能差的問題,同時(shí)也解決了 mixin 的可閱讀性差的問題。這里也有對(duì)于 React 最大的變化是函數(shù)式組件可以有自己的狀態(tài),扁平化的邏輯組織方式,更加友好地支持 TS 類型聲明。
在運(yùn)用Hooks的時(shí)候,除了 React 官方提供的,同時(shí)也支持我們能根據(jù)自己的業(yè)務(wù)場(chǎng)景自定義 Hooks,還有一些通用的 Hooks,例如用于請(qǐng)求的useRequest,用于定時(shí)器的useTimeout,用于節(jié)流的useThrottle等。于是出現(xiàn)了大量的 Hooks 庫,ahooks是其中比較受歡迎的 Hooks 庫之一,其提供了大量的 Hooks,基本滿足了大多數(shù)場(chǎng)景的需求。又是國人開發(fā),中文文檔友好,在我們團(tuán)隊(duì)的一些項(xiàng)目中就使用了 ahooks。
其中最常用的 hooks 就是useRequest,用于從后端請(qǐng)求數(shù)據(jù)的業(yè)務(wù)場(chǎng)景,除了簡單的數(shù)據(jù)請(qǐng)求,它還支持:
輪詢
防抖和節(jié)流
錯(cuò)誤重試
SWR(stale-while-revalidate)
緩存
等功能,這樣看起來是不是基本上滿足了我們請(qǐng)求后端數(shù)據(jù)需要考慮的大多數(shù)場(chǎng)景,其中還有 loading-delay、頁面 foucs 重新刷新數(shù)據(jù)等這些功能,就個(gè)人看法上面的功能才是使用比較頻繁的功能點(diǎn)。
一個(gè) Hooks 實(shí)現(xiàn)這么多功能,不禁感嘆它的強(qiáng)大,所以本文就從源碼的角度帶大家了解 useRequest 的實(shí)現(xiàn)。
架構(gòu)圖
下面是關(guān)于了解其模塊設(shè)計(jì),對(duì)于一個(gè)功能復(fù)雜的 API,如果不使用合適的架構(gòu)和方式組織代碼,其擴(kuò)展性和可維護(hù)性肯定比較差。功能點(diǎn)實(shí)現(xiàn)和核心代碼混在一起,閱讀代碼的人也無從下手,也帶來更大的測(cè)試難度。雖然 useRequest 只是一個(gè) Hook,但是實(shí)際上其設(shè)計(jì)還是有清晰的架構(gòu),我們來看看 useRequest 的架構(gòu)圖:
將 useRequest 的模塊劃分為三大塊:Core、Plugins、utils,然后 useRequest 將這些模塊組合在一起實(shí)現(xiàn)核心功能。
先看插件部分,看到每個(gè)插件的命名,如果了解 useRequest 的功能就會(huì)發(fā)現(xiàn),基本上每個(gè)功能點(diǎn)對(duì)應(yīng)一個(gè)插件。這也是 useRequest 設(shè)計(jì)比較巧妙的一點(diǎn),通過插件化機(jī)制降低了每個(gè)功能之間的耦合度,也降低了其本身的復(fù)雜度。這些點(diǎn)我們?cè)诜治鼍唧w的源碼的時(shí)候會(huì)再詳細(xì)介紹。
另外一部分核心的代碼我將其歸類為 Core(在 useRequest 的源碼中沒有這個(gè)名詞),主要實(shí)現(xiàn)了一個(gè) Fetch 類,這個(gè)類是 useRequest 的插件化機(jī)制實(shí)現(xiàn)和其它功能的核心實(shí)現(xiàn)。
下面我們深入源碼,看下其實(shí)現(xiàn)原理。
源碼解析
先看 Core 部分的源碼,主要是 Fetch 這個(gè)類的實(shí)現(xiàn)。
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 的設(shè)計(jì)特點(diǎn)就是簡潔,實(shí)際上有些 API 就是直接從 useRequest 暴露給外部用戶使用的,比如 run、runAsync、cancel、refresh、refreshAsync、mutate 等。像 runPluginHandler、setState 等 API 主要是給內(nèi)部用的 API,然它也有區(qū)分的做法,但從封裝的來說設(shè)計(jì)感并不好。
重點(diǎn)關(guān)注下幾個(gè) Fetch 類的屬性,一個(gè)是 state,它的類型是FetchState<TData, TParams>,一個(gè)是 pluginImpls,它是PluginReturn<TData, TParams>數(shù)組,實(shí)際上這個(gè)屬性就用來存所有插件執(zhí)行后返回的結(jié)果。還有一個(gè) count 屬性,是number類型,這個(gè)不看源代碼,是無法知道做什么的。這點(diǎn)useRequest 開發(fā)者做的不夠好。注釋也很少,全靠閱讀者深入到源碼,去看使用的地方,才能知道一些方法和屬性的作用。
那我們先來看下FetchState<TData, TParams>的定義,它定義在 src/type.ts 里面:
export interface FetchState<TData, TParams extends any[]> { loading: boolean; params?: TParams; data?: TData; error?: Error; }
這個(gè)定義就十分簡單了,就是存一個(gè)請(qǐng)求結(jié)果的上下文信息,其實(shí)這些信息需要暴露給外部用戶的,例如loading、data、errors等不就是我們使用 useRequest 經(jīng)常需要拿到的數(shù)據(jù)信息:
const { data, error, loading } = useRequest(service);
而對(duì)應(yīng)的 Fetch 封裝了 setState API,實(shí)際上就是用來更新 state 的數(shù)據(jù):
setState(s: Partial<FetchState<TData, TParams>> = {}) { this.state = { ...this.state, ...s, }; // ? 未知 this.subscribe(); }
除了更新 state,這里還調(diào)用了一個(gè) subscribe 方法,這是初始化 Fetch 類的時(shí)候傳進(jìn)來的一個(gè)參數(shù),它的類型是Subscribe,等后面將到調(diào)用的地方再看這個(gè)方法是怎么實(shí)現(xiàn)的,以及它的作用。
再看下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; }
上面其實(shí)很簡單,就都是一些回調(diào)鉤子,從名字對(duì)應(yīng)上來看,對(duì)應(yīng)了請(qǐng)求的各個(gè)階段,除了onMutate是其內(nèi)部擴(kuò)展的一個(gè)鉤子。
也就是說 pluginImpls 里面存的是一堆含有各個(gè)鉤子函數(shù)的對(duì)象集合,如果技術(shù)敏銳的同學(xué),可能很容易就想到發(fā)布訂閱模式,這不就是存了一系列的 subscribe 回調(diào),這不過這是一個(gè)回調(diào)的集合,里面有各種不同請(qǐng)求階段的回調(diào)。那么到底是不是這樣,我們繼續(xù)往下看。
要搞清楚 Fetch 的運(yùn)作方式,我們需要看兩個(gè)核心 API 的實(shí)現(xiàn):runPluginHandler和runAsync,其它所有的 API 實(shí)際上都在調(diào)用這兩個(gè) 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); }
這個(gè)代碼就十分簡單了,就兩行代碼。這里用到的就是接收一個(gè) event 參數(shù),它的類型就是keyof PluginReturn<TData, TParams>,也就是:onBefore | onRequest | onSuccess | onError | onFinally | onCancel | onMutate的聯(lián)合類型,以及其它額外的參數(shù),然后從 pluginImpls 中找出所有對(duì)應(yīng)的 event 回調(diào)鉤子函數(shù),然后執(zhí)行回調(diào)函數(shù),拿到結(jié)果并返回。
再看runAsync的實(shí)現(xiàn):
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; } }
現(xiàn)在我們先說下上面代碼,這個(gè)函數(shù)實(shí)際上做的事就是調(diào)用我們傳入的獲取數(shù)據(jù)的方法,然后拿到成功或者失敗的結(jié)果,進(jìn)行一系列的數(shù)據(jù)處理,然后更新到 state,執(zhí)行插件的各回調(diào)鉤子,還有就是我們通過 options 傳入的回調(diào)函數(shù)。
這樣說文字不知道大家是否聽的懂,現(xiàn)在我分請(qǐng)求階段分析代碼。
首先前兩行是對(duì) count 屬性的累加處理,詳細(xì)不在這里說,等后面看到 currentCount 的使用的地方,我們?cè)僬f。
onBefore
接下來 5~27 行實(shí)際上是對(duì) onBefore 回調(diào)鉤子的執(zhí)行,這樣就可以拿到結(jié)果做的一些邏輯處理。這里調(diào)用的就是 runPluginHandler 方法,傳入的參數(shù)是 onBefore 和外部用戶定義的 params 參數(shù)。然后執(zhí)行完所有的 onBefore 鉤子函數(shù),拿到最后的結(jié)果,如果 stopNow 的 flag 是 true,則直接返回沒有結(jié)果的 Promise。看注釋,我們知道這里實(shí)際上做的是取消請(qǐng)求的處理,當(dāng)我們?cè)?onBefore 的鉤子里實(shí)現(xiàn)了取消的邏輯,符合條件后并會(huì)真正的阻斷請(qǐng)求。
當(dāng)然如果沒有取消,然后接著更新 state 數(shù)據(jù),如果立即返回的 returnNow flag 為 true,則立馬將更新后的 state 返回,否則執(zhí)行用戶傳入的 options 中的 onBefore 回調(diào),也就是說在調(diào)用 useRequest 的時(shí)候,我們可以通過 options 參數(shù)傳入 onBefore 函數(shù),進(jìn)行請(qǐng)求之前的一些邏輯處理。
onRequest
現(xiàn)在就是真正執(zhí)行請(qǐng)求數(shù)據(jù)的方法了,這里就會(huì)執(zhí)行所有的 onRequest 鉤子。實(shí)際上,通過 onRequest 鉤子我們是可以重寫傳入的獲取數(shù)據(jù)的方法,因?yàn)樽詈髨?zhí)行的是 onRequest 回調(diào)返回的servicePromise。
拿到最后執(zhí)行的請(qǐng)求數(shù)據(jù)方法,就開始發(fā)起請(qǐng)求。在這里發(fā)現(xiàn)了前面的 currentCount 的使用,它會(huì)去對(duì)比當(dāng)前最新的 count 和執(zhí)行這個(gè)方法時(shí)定義的 currentCount 是否相等,如果不相等,則會(huì)做類似于取消請(qǐng)求的處理。這里大概知道 count 的作用類似于一個(gè)”鎖“的作用,我的理解是,如果在執(zhí)行這些代碼過程有產(chǎn)生一些比這里優(yōu)先級(jí)更高的處理邏輯或者請(qǐng)求操作,是需要 cancel 掉這次的請(qǐng)求,以最新的請(qǐng)求為準(zhǔn)。當(dāng)然,最后還是要看哪些地方可能會(huì)修改 count。
onSuccess
執(zhí)行完請(qǐng)求后,如果請(qǐng)求成功,則拿到請(qǐng)求返回的數(shù)據(jù),更新到 state,執(zhí)行用戶傳入的成功回調(diào)和各插件的成功回調(diào)鉤子。
onFinally
成功之后,執(zhí)行 onFinally 鉤子,這里也很嚴(yán)謹(jǐn),也會(huì)比較 count 的值,確保一致之后,才會(huì)執(zhí)行各插件的回調(diào)鉤子,預(yù)發(fā)一些”競(jìng)態(tài)“情況的發(fā)生。
onError
如果請(qǐng)求失敗,就會(huì)進(jìn)入到 catch 分支,執(zhí)行一些處理錯(cuò)誤的邏輯,更新 error 信息到 state 中。同樣這里也會(huì)有 count 的對(duì)比,然后執(zhí)行 onError 的回調(diào)。執(zhí)行完 onError 也會(huì)同樣執(zhí)行 onFinally 的回調(diào),因?yàn)橐粋€(gè)請(qǐng)求要么成功,要么失敗,都會(huì)需要執(zhí)行最后的 onFinally 回調(diào)。
其它 API
其它的例如 run、cancel、refresh 等 API,實(shí)際上調(diào)用的是runPluginHandler和runAsyncAPI,例如 run:
run(...params: TParams) { this.runAsync(...params).catch((error) => { if (!this.options.onError) { console.error(error); } }); }
代碼很容易看懂,就不過多介紹。
我們來看看 cancel 的實(shí)現(xiàn):
cancel() { this.count += 1; this.setState({ loading: false, }); this.runPluginHandler('onCancel'); }
最后的 runPluginHandler 調(diào)用的作用我們十分明白,要注意的是對(duì) count 的修改。前面我們提到每次 runAsync 一些核心階段會(huì)判斷 count 是否和 currentCount 能對(duì)得上,看到這里我們就徹底明白了 count 的作用了。實(shí)際上在我們執(zhí)行了 run 的操作,如果在本次 runAsync 方法執(zhí)行過程中,我們就調(diào)用了 cancel 方法,那么無論是在請(qǐng)求發(fā)起前還是后,都會(huì)把本次執(zhí)行當(dāng)做 cancel 處理,返回空的數(shù)據(jù)。也就是說,這個(gè) count 就是為了實(shí)現(xiàn)請(qǐng)求取消功能的一個(gè)標(biāo)識(shí)。
小結(jié)
其實(shí)這里了解runAsync的實(shí)現(xiàn),實(shí)際基本上整個(gè)的 Fetch 的核心邏輯也看的清楚。從一個(gè)請(qǐng)求的生命周期角度來看,這里主要做兩件事:
執(zhí)行各階段的鉤子回調(diào);
更新數(shù)據(jù)到 state。
其實(shí)這都?xì)w功于 useRequest 的巧妙設(shè)計(jì),我們看這部分源碼,只要看懂了類型和兩個(gè)核心的方法,都不用關(guān)心具體每個(gè)插件的實(shí)現(xiàn)。它將每個(gè)功能點(diǎn)的復(fù)雜度和核心的邏輯通過插件機(jī)制隔離開來,從而每個(gè)插件只需要按一定的契約實(shí)現(xiàn)好自己的功能就行,然后 Fetch 不管有多少插件,只負(fù)責(zé)在合適的時(shí)間點(diǎn)調(diào)用插件鉤子,做到了完全的解耦。
plugins
其實(shí)看完了 Fetch,還沒看插件,你腦子里就大概知道怎么去實(shí)現(xiàn)一個(gè)插件。因?yàn)椴寮容^多,限于篇幅原因,這里就以 usePollingPlugin 和 useRetryPlugin 兩個(gè)插件為例,進(jìn)行詳細(xì)的源碼介紹。
usePollingPlugin
首先需要清楚一點(diǎn)每個(gè)插件實(shí)際也是一個(gè) Hook,所以在它內(nèi)部可以使用任何 Hook 的功能或者調(diào)用其它 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(); }, }; };
它接受兩個(gè)參數(shù),一個(gè)是 fetchInstance,也就是前面提到的 Fetch 實(shí)例,第二個(gè)參數(shù)是 options,支持傳入 pollingInterval、pollingWhenHidden 兩個(gè)屬性。這兩個(gè)屬性從命名上比較容易理解,一個(gè)就是輪詢的時(shí)間間隔,另外一個(gè)猜測(cè)應(yīng)該是可以在某種場(chǎng)景下通過設(shè)置這個(gè) flag 停止輪詢。在真實(shí)的場(chǎng)景中,確實(shí)有比如要求用戶在切換到其它 tab 頁時(shí)停止輪詢等這樣的需求。所以這個(gè)配置,還比較好理解。
而每個(gè)插件的作用就是在請(qǐng)求的各個(gè)階段進(jìn)行定制化的邏輯處理,以輪詢?yōu)槔渥詈诵牡倪壿嬙谟?onFinally 的回調(diào),在每次請(qǐng)求結(jié)束后,設(shè)置一個(gè) setTimeout,然后按用戶傳入的 pollingInterval 進(jìn)行定時(shí)執(zhí)行 Fetch 的 refresh 方法。
還有就是停止輪詢的時(shí)機(jī),每次用戶主動(dòng)取消請(qǐng)求,在 onCancel 的回調(diào)停止輪詢。如果已經(jīng)開始了輪詢,在每次新的請(qǐng)求調(diào)用的時(shí)候先停止上一次的輪詢,避免重復(fù)。當(dāng)然包括,如果組件修改了 pollingInterval 等的時(shí)候,需要先停止掉之前的輪詢。
useRetryPlugin
假設(shè)讓你去設(shè)計(jì)一個(gè) retry 的插件,那么你的設(shè)計(jì)思路是什么了?需要關(guān)注的核心邏輯是什么?還是前面那句話: 每個(gè)插件的作用就是在請(qǐng)求的各個(gè)階段進(jìn)行定制化的邏輯處理,那如果要實(shí)現(xiàn) retry 肯定你首要關(guān)注的是,什么時(shí)候才需要 retry?答案顯而易見,那就是請(qǐng)求失敗的時(shí)候,也就是需要在 onError 回調(diào)實(shí)現(xiàn) retry 的邏輯。考慮得周全一點(diǎn),你還需要知道 retry 的次數(shù),因?yàn)榈诙我部赡苁×恕.?dāng)然還有就是 retry 的時(shí)間間隔,失敗后多久 retry?這些是外部使用者關(guān)心的,所以應(yīng)該將它們?cè)O(shè)計(jì)成配置項(xiàng)。
分析好了需求,我們看下 retry 插件的實(shí)現(xiàn):
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 指數(shù)補(bǔ)償 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); } }, }; };
第一個(gè)參數(shù)跟 usePollingPlugin 的插件一樣,都是接收 Fetch 實(shí)例,第二個(gè)參數(shù)是 options,支持 retryInterval、retryCount 等選型,從命名上看跟我們剛開始分析需求的時(shí)候想的差不多。
看代碼,核心的邏輯主要是在 onError 的回調(diào)中。首先前面定義了一個(gè) countRef,記錄 retry 的次數(shù)。執(zhí)行了 onError 回調(diào),代表新的一次請(qǐng)求錯(cuò)誤發(fā)生,然后判斷如果 retryCount 為 -1,或者當(dāng)前 retry 的次數(shù)還小于用戶自定義的次數(shù),則通過一個(gè)定時(shí)器設(shè)置下次 retry 的時(shí)間,否則將 countRef 重置。
還需要注意的是其它的一些回調(diào)的處理,比如當(dāng)請(qǐng)求成功或者被取消,需要重置 countRef,取消的時(shí)候還需要清理可能存在的下一次 retry 的定時(shí)器。
這里 onBefore 的邏輯處理怎么理解了?首先這里會(huì)有一個(gè) triggerByRetry 的 flag,如果 flag 是 false。則會(huì)清空 countRef。然后會(huì)將 triggerByRetry 設(shè)置為 false,然后清理掉上一次可能存在的 retry 定時(shí)器。我個(gè)人的理解是這里設(shè)置一個(gè) flag 是為了避免如果 useRequest 重新執(zhí)行,導(dǎo)致請(qǐng)求重新發(fā)起,那么在 onBefore 的時(shí)候需要做一些重置處理,以防和上一次的 retry 定時(shí)器撞車。
小結(jié)
其它插件的設(shè)計(jì)思路是類似的,關(guān)鍵是要分析出你需要實(shí)現(xiàn)的功能是作用在請(qǐng)求的哪個(gè)階段,那么就需要在這個(gè)鉤子里實(shí)現(xiàn)核心的邏輯處理。然后再考慮其它鉤子的一些重置處理,取消處理等,所以在優(yōu)秀合理的設(shè)計(jì)下實(shí)現(xiàn)某個(gè)功能它的成本是很低的,而且也不需要關(guān)心其它插件的邏輯,這樣每個(gè)插件也是可以獨(dú)立測(cè)試的。
useRequest
分析了核心的兩塊源碼,我們來看下,怎么組裝最后的 useRequest。首先在 useRequest 之前,還有一層抽象叫 useRequestImplement,看下是怎么實(shí)現(xiàn)的:
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 循環(huán)去執(zhí)行每個(gè)插件 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>; }
前面兩個(gè)參數(shù)如果使用過 useRequest 的都知道,就是我們通常傳給 useRequest 的參數(shù),一個(gè)是請(qǐng)求 api,一個(gè)就是 options。這里還多了個(gè)插件參數(shù),大概可以知道,內(nèi)置的一些插件應(yīng)該會(huì)在更上層的地方傳進(jìn)來,做一些參數(shù)初始化的邏輯。
然后通過 useLatest 構(gòu)造一個(gè) serviceRef,保證能拿到最新的 service。接下來,使用 useUpdate Hook 創(chuàng)建了update 方法,然后再創(chuàng)建 fetchInstance 的時(shí)候作為第三個(gè)參數(shù)傳遞給 Fetch,這里就是我們前面提到過的 subscribe。那我們要看下 useUpdate 做了什么:
const useUpdate = () => { const [, setState] = useState({}); return useCallback(() => setState({}), []); };
原來是個(gè)”黑科技“,類似 class 組件的 $forceUpdate API,就是通過 setState,讓組件強(qiáng)行渲染一次。
接著就是使用 useMount,如果發(fā)現(xiàn)用戶沒有設(shè)置 manual 或者將其設(shè)置為 false,立馬會(huì)執(zhí)行一次請(qǐng)求。當(dāng)組件被銷毀的時(shí)候,在 useUnMount 中進(jìn)行請(qǐng)求的取消。最后返回暴露給用戶的數(shù)據(jù)和 API。
最后看下 useRequest 的實(shí)現(xiàn):
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>[]); }
這里就會(huì)把內(nèi)置的插件傳入進(jìn)去,當(dāng)然還有用戶自定義的插件。實(shí)際上 useRequest 是支持用戶自定義插件的,這又突出了插件化設(shè)計(jì)的必要性。除了能降低本身自己的功能之間的復(fù)雜度,也能提供更多的靈活度給到用戶,如果你覺得功能不夠,實(shí)現(xiàn)自定義插件吧。
對(duì)自定義 hook 的思考
面向?qū)ο缶幊汤锩嬗幸粋€(gè)原則叫職責(zé)單一原則, 我個(gè)人理解它的含義是我們?cè)谠O(shè)計(jì)一個(gè)類或者一個(gè)方法時(shí),它的職責(zé)應(yīng)該盡量單一。如果一個(gè)類的抽象不在一個(gè)層次,那么這個(gè)類注定會(huì)越來越膨脹,難以維護(hù)。一個(gè)方法職責(zé)越單一,它的復(fù)用性就可能越高,可測(cè)試性也越好。
其實(shí)我們?cè)谠O(shè)計(jì)一個(gè) hooks,也是需要參照這個(gè)原則的。Hooks API 出現(xiàn)的一個(gè)重大意義,就是解決我們?cè)诰帉懡M件時(shí)的邏輯復(fù)用問題。沒有 Hooks,之前是使用 HOC、Render props或者 Mixin 等解決邏輯復(fù)用的問題,然而每一種方式在大量實(shí)踐后都發(fā)現(xiàn)有明顯的缺點(diǎn)。所以,我們?cè)谧远x一個(gè) Hook 時(shí),總是應(yīng)該朝著提高復(fù)用性的角度出發(fā)。
光說太抽象,舉個(gè)之前我在業(yè)務(wù)開發(fā)中遇到的一個(gè)例子。在一個(gè)項(xiàng)目中,我們封裝了一個(gè)計(jì)算預(yù)算的 Hook 叫useBudgetValidate,不方便貼所有代碼,下面通過偽代碼列下這個(gè) Hook 做的事:
export default function useBudgetValidate({ id, dailyBudgetType, mode }: Options) { const [dailyBudgetSetting, setDailyBudgetSetting] = useState<BudgetSetting | null>(null); // 從后端獲取某個(gè)數(shù)據(jù) const { data: adSetCountRes } = useRequest( (campaign: ReactText) => getSomeData({ params: { id } })); // 從后端獲取預(yù)算配置 useRequest( () => { return getBudgetSetting(); }, { onSuccess: result => setDailyBudgetSetting(result), }, ); /** * 對(duì)于傳入的預(yù)算的類型, 返回的預(yù)算設(shè)置 */ const currentDailyBudgetSetting: DailyBudgetSetting | undefined = useMemo(() => { if (dailyBudgetType === BudgetTypeEnum.AdSet) { return dailyBudgetSetting?.adset; } if (dailyBudgetType === BudgetTypeEnum.Smart) { return dailyBudgetSetting?.smart; } const campaignBudget = dailyBudgetSetting?.campaign; // 這里有大量的計(jì)算邏輯,得到最后的 campaignBudget return campaignBudget; }, []); return { currentDailyBudgetSetting, dailyBudgetSetting, }; }
上面的Hook 就是從后端獲取數(shù)據(jù),然后根據(jù)不同的傳參進(jìn)行預(yù)算計(jì)算,然后返回預(yù)算信息。可現(xiàn)在有個(gè)問題影響,因?yàn)橛?jì)算預(yù)算是項(xiàng)目通用的邏輯。在另外一個(gè)頁面也需要這段計(jì)算邏輯,但是那個(gè)頁面已經(jīng)從后端其它的接口獲取了預(yù)算信息,或者通過其它方式構(gòu)造了計(jì)算預(yù)算需要的數(shù)據(jù)。因此核心矛盾點(diǎn)在于很多頁面依賴這段計(jì)算邏輯,但是數(shù)據(jù)來源是不一致的。將獲取預(yù)算配置和其它信息的接口邏輯放在這個(gè) Hook 里面就會(huì)導(dǎo)致它的職責(zé)不單一,所以沒法很容易在其它場(chǎng)景復(fù)用。
現(xiàn)在就說說重構(gòu)的思路,就是將數(shù)據(jù)請(qǐng)求的邏輯抽離,多帶帶封裝一個(gè) Hook,或者把職責(zé)交給組件去做。這個(gè) Hook 只做一件事,那就是接收配置和其它參數(shù),進(jìn)行預(yù)算計(jì)算,將結(jié)果返回給外面。
現(xiàn)在有個(gè)復(fù)雜又難解的就是useRequest的功能Hook,從功能上看,感覺它既做了一般請(qǐng)求數(shù)據(jù)的功能,但同時(shí)又做了輪詢,做了緩存,做了重試,做了。。。簡而言之就是很多的職責(zé)。
但他們都依賴請(qǐng)求這個(gè)關(guān)鍵點(diǎn),這也就表明它們的抽象是在同一層次上。而且 useRquest 是一個(gè)更加通用的 Hook,它作為一個(gè) package 給大量的用戶使用。如果你是一個(gè)使用者,你想要什么能力,它就可以實(shí)現(xiàn)什么,超級(jí)爽。
在Philosophy of Software Design一書中提到一個(gè)概念叫:深模塊,它的意思是:深模塊是那些既提供了強(qiáng)大功能但又有著簡單接口的模塊。在設(shè)計(jì)一些模塊或者 API 的時(shí)候,比如像 useRequest 這種,那么就要符合這個(gè)原則,用戶只需要少量的配置,就能使用各插件帶來的豐富功能。
所以最后,總結(jié)下:如果我們?cè)谌粘I(yè)務(wù)開發(fā)封裝一些 Hook,要記住應(yīng)該盡量保證職責(zé)單一,以提高其復(fù)用性。如果我們需要設(shè)計(jì)一個(gè)抽象程度很高,然后給多個(gè)項(xiàng)目使用的 Hook,那么在設(shè)計(jì)的時(shí)候,應(yīng)該符合深模塊的特點(diǎn),接口盡量簡單,又需要滿足各需求場(chǎng)景,將功能復(fù)雜度隱藏在 Hook 內(nèi)部。
總結(jié)
我們現(xiàn)在降的就是從 Fetch 類的實(shí)現(xiàn)和 plugins 的設(shè)計(jì)詳細(xì)解析了 useRequest 的源碼。
useRequest 核心源碼主要在 Fetch 類的實(shí)現(xiàn)中,主要是通過巧妙的將請(qǐng)求劃分為各個(gè)階段的設(shè)計(jì),之后將豐富的功能交給每個(gè)插件去實(shí)現(xiàn),解耦功能之間的關(guān)系,降低本身維護(hù)的復(fù)雜度,提高可測(cè)試性;
useRequest 雖然只是一個(gè)代碼千行左右的 Hook,但是通過插件化機(jī)制,使得各個(gè)功能之間完全解耦,提高了代碼的可維護(hù)性和可測(cè)試性,同時(shí)也提供了用戶自定義插件的能力;
職責(zé)單一的原則在任何場(chǎng)景下引用都不會(huì)過時(shí),我們?cè)谠O(shè)計(jì)一些 Hook 的時(shí)候應(yīng)該也要考慮單一原則。但是在設(shè)計(jì)一些跨多項(xiàng)目通用的 Hook,應(yīng)該朝著深模塊的角度設(shè)計(jì),提供簡單的接口,把復(fù)雜度隱藏在模塊內(nèi)部。
知識(shí)點(diǎn)都已講述了,只看每個(gè)人自己的理解。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/128265.html
起因 社會(huì)在不斷的向前,技術(shù)也在不斷的完善進(jìn)步。從 React Hooks 正式發(fā)布到現(xiàn)在,越來越多的項(xiàng)目正在使用 Function Component 替代 Class Component,Hooks 這一新特性也逐漸被廣泛的使用。 這樣的解析是不是很熟悉,在日常中時(shí)常都有用到,但也有一個(gè)可以解決這樣重復(fù)的就是對(duì)數(shù)據(jù)請(qǐng)求的邏輯處理,對(duì)防抖節(jié)流的邏輯處理等。 另一方面,由于 Hoo...
我們今天來講講關(guān)于ahooks 源碼,我們目標(biāo)主要有以下幾點(diǎn): 深入了解 React hooks。 明白如何抽象自定義 hooks,且可以構(gòu)建屬于自己的 React hooks 工具庫。 小建議:培養(yǎng)閱讀學(xué)習(xí)源碼的習(xí)慣,工具庫是一個(gè)對(duì)源碼閱讀不錯(cuò)的選擇。 列表頁常見元素 后臺(tái)管理系統(tǒng)中常見典型列表頁包括篩選表單項(xiàng)、Table表格、Pagination分頁這三部分。 針對(duì)使用 Ant...
我們講下 ahooks 的核心 hook —— useRequest。 useRequest 簡介 根據(jù)官方文檔的介紹,useRequest 是一個(gè)強(qiáng)大的異步數(shù)據(jù)管理的 Hooks,React 項(xiàng)目中的網(wǎng)絡(luò)請(qǐng)求場(chǎng)景使用 useRequest ,這就可以。 useRequest通過插件式組織代碼,核心代碼極其簡單,并且可以很方便的擴(kuò)展出更高級(jí)的功能。目前已有能力包括: 自動(dòng)請(qǐng)求/手動(dòng)請(qǐng)求 ...
這是講 ahooks 源碼的第一篇文章,簡要就是以下幾點(diǎn): 加深對(duì) React hooks 的理解。 學(xué)習(xí)如何抽象自定義 hooks。構(gòu)建屬于自己的 React hooks 工具庫。 培養(yǎng)閱讀學(xué)習(xí)源碼的習(xí)慣,工具庫是一個(gè)對(duì)源碼閱讀不錯(cuò)的選擇。 注:本系列對(duì) ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對(duì)源碼做了一些解讀,可見詳情。 第一篇主要介紹 a...
我們講述的是關(guān)于 ahooks 源碼系列文章的第七篇,總結(jié)主要講述下面幾點(diǎn): 鞏固 React hooks 的理解。 學(xué)習(xí)如何抽象自定義 hooks。構(gòu)建屬于自己的 React hooks 工具庫。 培養(yǎng)閱讀學(xué)習(xí)源碼的習(xí)慣,工具庫是一個(gè)對(duì)源碼閱讀不錯(cuò)的選擇。 注:本系列對(duì) ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對(duì)源碼做了一些解讀,可見詳情。 ...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28