與組件生命週期綁定的 Utils 很是適合基於 React Hooks 來作,好比能夠將 「發請求」 這個功能與組件生命週期綁定,實現一些便捷的功能。前端
此次以 @umijs/use-request 爲例子,分析其功能思路與源碼。git
@umijs/use-request 支持如下功能:github
loading
, data
, error
狀態。options.manual = true
, 則手動調用 run
時纔會取數。options.pollingInterval
則進入輪詢模式,可經過 run
/ cancel
開始與中止輪詢。options.fetchKey
能夠對請求狀態隔離,經過 fetches
拿到全部請求狀態。options.debounceInterval
開啓防抖。options.throttleInterval
開啓節流。options.cacheKey
後開啓對請求結果緩存機制,下次請求前會優先返回緩存並在後臺從新取數。options.cacheKey
全局共享,能夠提早執行 run
實現預加載效果。options.refreshOnWindowFocus = true
在瀏覽器 refocus
與 revisible
時從新請求。mutate
直接修改取數結果。options.loadingDelay
能夠延遲 loading
變成 true
的時間,有效防止閃爍。options.refreshDeps
能夠在依賴變更時從新觸發請求。options.paginated
可支持翻頁場景。options.loadMore
可支持加載更多場景。一切 Hooks 的功能拓展都要基於 React Hooks 生命週期,咱們能夠利用 Hooks 作下面幾件與組件相關的事:數組
上面這些功能就能夠基於這些基礎能力拓展了:瀏覽器
默認自動請求緩存
在組件初始時機取數。因爲和組件生命週期綁定,能夠很方便實現各組件相互隔離的取數順序強保證:能夠利用取數閉包存儲 requestIndex,取數結果返回後與當前最新 requestIndex 進行比對,丟棄不一致的取數結果。微信
手動觸發請求閉包
將觸發取數的函數抽象出來並在 CustomHook 中 return。async
輪詢請求函數
在取數結束後設定 setTimeout
從新觸發下一輪取數。
並行請求
每次取數時先獲取當前請求惟一標識 fetchKey
,僅更新這個 key 下的狀態。
請求防抖、請求節流
這個實現方式能夠挺通用化,即取數調用函數處替換爲對應 debounce
或 throttle
函數。
請求預加載
這個功能只要實現全局緩存就天然支持了。
屏幕聚焦從新請求
這個能夠統一監聽 window action 事件,並觸發對應組件取數。能夠全局統一監聽,也能夠每一個組件分別監聽。
請求結果突變
因爲取數結果存儲在 CustomHook 中,直接修改數據 data 值便可。
加載延遲
有加載延遲時,能夠先將 loading
設置爲 false
,等延遲到了再設置爲 true
,若是此時取數提早完畢則銷燬定時器,實現無 loading 取數。
自定義請求依賴
利用 useEffect
和自帶的 deps 便可。
分頁
基於通用取數 Hook 封裝,本質上是多帶了一些取數參數與返回值參數,並遵循 Antd Table 的 API。
加載更多
和分頁相似,區別是加載更多不會清空已有數據,而且須要根據約定返回結構 noMore
判斷是否能繼續加載。
接下來是源碼分析。
首先定義了一個類 Fetch
,這是由於一個 useRequest
的 fetchKey
特性能夠經過多實例解決。
Class 的生命週期不依賴 React Hooks,因此將不依賴生命週期的操做收斂到 Class 中,不只提高了代碼抽象程度,也提高了可維護性。
class Fetch<R, P extends any[]> {
// ...
// 取數狀態存儲處
state: FetchResult<R, P> = {
loading: false,
params: [] as any,
data: undefined,
error: undefined,
run: this.run.bind(this.that),
mutate: this.mutate.bind(this.that),
refresh: this.refresh.bind(this.that),
cancel: this.cancel.bind(this.that),
unmount: this.unmount.bind(this.that),
};
constructor(
service: Service<R, P>,
config: FetchConfig<R, P>,
// 外部經過這個回調訂閱 state 變化
subscribe: Subscribe<R, P>,
initState?: { data?: any; error?: any; params?: any; loading?: any }
) {}
// 此 setState 非彼 setState,做用是更新 state 並通知訂閱
setState(s = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe(this.state);
}
// 實際取數函數,但下劃線命名的帶有一些歷史氣息啊
_run(...args: P) {}
// 對外暴露的取數函數,對防抖和節流作了分發處理
run(...args: P) {
if (this.debounceRun) {
// return ..
}
if (this.throttleRun) {
// return ..
}
return this._run(...args);
}
// 取消取數,考慮到了防抖、節流兼容性
cancel() {}
// 以上次取數參數從新取數
refresh() {}
// 輪詢 starter
rePolling() {}
// 對應 mutate 函數
mutate(data: any) {}
// 銷燬訂閱
unmount() {}
}
複製代碼
默認自動請求
經過 useEffect
零依賴實現,須要:
fetches
獨立管理。// 第一次默認執行
useEffect(() => {
if (!manual) {
// 若是有緩存
if (Object.keys(fetches).length > 0) {
/* 從新執行全部的 */
Object.values(fetches).forEach((f) => {
f.refresh();
});
} else {
// 第一次默認執行,能夠經過 defaultParams 設置參數
run(...(defaultParams as any));
}
}
}, []);
複製代碼
默認執行第 11 行,並根據當前的 fetchKey
生成對應 fetches
,若是初始化已經存在 fetches
,則行爲改成從新執行全部 已存在的 並行請求。
手動觸發請求
上一節已經在初始請求時禁用了 manual
開啓時的默認取數。下一步只要將封裝的取數函數 run
定義出來並暴露給用戶:
const run = useCallback(
(...args: P) => {
if (fetchKeyPersist) {
const key = fetchKeyPersist(...args);
newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
}
const currentFetchKey = newstFetchKey.current;
// 這裏必須用 fetchsRef,而不能用 fetches。
// 不然在 reset 完,當即 run 的時候,這裏拿到的 fetches 是舊的。
let currentFetch = fetchesRef.current[currentFetchKey];
if (!currentFetch) {
const newFetch = new Fetch(
servicePersist,
config,
subscribe.bind(null, currentFetchKey),
{
data: initialData,
}
);
currentFetch = newFetch.state;
setFeches((s) => {
// eslint-disable-next-line no-param-reassign
s[currentFetchKey] = currentFetch;
return { ...s };
});
}
return currentFetch.run(...args);
},
[fetchKey, subscribe]
);
複製代碼
主動取數函數與內部取數函數共享一個,因此 run
函數要考慮多種狀況,其中之一就是並行取數的狀況,所以須要拿到當前取數的 fetchKey
,並建立一個 Fetch
的實例,最終調用 Fetch
實例的 run
函數取數。
輪詢請求
輪詢取數在 Fetch
實際取數函數 _fetch
中定義,當取數函數 fetchService
(對多種形態的取數方法進行封裝後)執行完後,不管正常仍是報錯,都要進行輪詢邏輯,所以在 .finally
時機裏判斷:
fetchService.then().finally(() => {
if (!this.unmountedFlag && currentCount === this.count) {
if (this.config.pollingInterval) {
// 若是屏幕隱藏,而且 !pollingWhenHidden, 則中止輪詢,並記錄 flag,等 visible 時,繼續輪詢
if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
this.pollingWhenVisibleFlag = true;
return;
}
this.pollingTimer = setTimeout(() => {
this._run(...args);
}, this.config.pollingInterval);
}
}
});
複製代碼
輪詢還要考慮到屏幕是否隱藏,若是能夠觸發輪詢則觸發定時器再次調用 _run
,注意這個定時器須要正常銷燬。
並行請求
每一個 fetchKey
對應一個 Fetch
實例,這個邏輯在 手動觸發請求 介紹的 run
函數中已經實現。
這塊的封裝思路能夠品味一下,從外到內分別是 React Hooks 的 fetch -> Fetch 類的 run -> Fetch 類的 _run,並行請求作在 React Hooks 這一層。
請求防抖、請求節流
這個實現就在 Fetch 類的 run
函數中:
function run(...args: P) {
if (this.debounceRun) {
this.debounceRun(...args);
return Promise.resolve(null as any);
}
if (this.throttleRun) {
this.throttleRun(...args);
return Promise.resolve(null as any);
}
return this._run(...args);
}
複製代碼
因爲防抖和節流是 React 無關的,也不是最終取數無關的,所以實如今 run
這個夾層函數進行分發。
這裏實現的比較簡化,防抖後 run
拿到的 Promise 再也不是有效的取數結果了,其實這塊仍是能夠進一步對 Promise 進行封裝,不管在防抖仍是正常取數的場景都返回 Promise,只需 resolve 的時機由 Fetch
這個類靈活把控便可。
請求預加載
預加載就是緩存機制,首先利用 useEffect
同步緩存:
// cache
useEffect(() => {
if (cacheKey) {
setCache(cacheKey, {
fetches,
newstFetchKey: newstFetchKey.current,
});
}
}, [cacheKey, fetches]);
複製代碼
在初始化 Fetch
實例時優先採用緩存:
const [fetches, setFeches] = useState<Fetches<U, P>>(() => {
// 若是有 緩存,則從緩存中讀數據
if (cacheKey) {
const cache = getCache(cacheKey);
if (cache) {
newstFetchKey.current = cache.newstFetchKey;
/* 使用 initState, 從新 new Fetch */
const newFetches: any = {};
Object.keys(cache.fetches).forEach((key) => {
const cacheFetch = cache.fetches[key];
const newFetch = new Fetch();
// ...
newFetches[key] = newFetch.state;
});
return newFetches;
}
}
return [];
});
複製代碼
屏幕聚焦從新請求
在 Fetch
構造函數實現監聽並調用 refresh
便可,源碼裏採起全局統一監聽的方式:
function subscribe(listener: () => void) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
let eventsBinded = false;
if (typeof window !== "undefined" && window.addEventListener && !eventsBinded) {
const revalidate = () => {
if (!isDocumentVisible()) return;
for (let i = 0; i < listeners.length; i++) {
// dispatch 每一個 listener
const listener = listeners[i];
listener();
}
};
window.addEventListener("visibilitychange", revalidate, false);
// only bind the events once
eventsBinded = true;
}
複製代碼
在 Fetch
構造函數裏註冊:
this.limitRefresh = limit(this.refresh.bind(this), this.config.focusTimespan);
if (this.config.pollingInterval) {
this.unsubscribe.push(subscribeVisible(this.rePolling.bind(this)));
}
複製代碼
並經過 limit
封裝控制調用頻率,並 push 到 unsubscribe
數組,一邊監聽能夠隨組件一塊兒銷燬。
請求結果突變
這個函數只要更新 data
數據結果便可:
function mutate(data: any) {
if (typeof data === "function") {
this.setState({
data: data(this.state.data) || {},
});
} else {
this.setState({
data,
});
}
}
複製代碼
值得注意的是,cancel
、refresh
、mutate
都必須在初次請求完成後纔有意義,因此初次返回的函數是一個拋錯:
const noReady = useCallback(
(name: string) => () => {
throw new Error(`Cannot call ${name} when service not executed once.`);
},
[]
);
return {
loading: !manual || defaultLoading,
data: initialData,
error: undefined,
params: [],
cancel: noReady("cancel"),
refresh: noReady("refresh"),
mutate: noReady("mutate"),
...(fetches[newstFetchKey.current] || {}),
} as BaseResult<U, P>;
複製代碼
等取數完成後會被 ...(fetches[newstFetchKey.current] || {})
這一段覆蓋爲正常函數。
加載延遲
若是設置了加載延遲,請求發動時就不該該當即設置爲 loading,這個邏輯寫在 _run
函數中:
function _run(...args: P) {
// 取消 loadingDelayTimer
if (this.loadingDelayTimer) {
clearTimeout(this.loadingDelayTimer);
}
this.setState({
loading: !this.config.loadingDelay,
params: args,
});
if (this.config.loadingDelay) {
this.loadingDelayTimer = setTimeout(() => {
this.setState({
loading: true,
});
}, this.config.loadingDelay);
}
}
複製代碼
啓動一個 setTimeout
將 loading 設爲 true
便可,這個 timeout 在下次執行 _run
時被 clearTimeout
清空。
自定義請求依賴
最明智的作法是利用 useEffect
實現,實際代碼作了組件 unmount 保護:
// refreshDeps 變化,從新執行全部請求
useUpdateEffect(() => {
if (!manual) {
/* 所有從新執行 */
Object.values(fetchesRef.current).forEach((f) => {
f.refresh();
});
}
}, [...refreshDeps]);
複製代碼
非手動條件下,依賴變化全部已存在的 fetche
執行 refresh
便可。
分頁和加載更多就不解析了,原理是在 useAsync
這個基礎請求 Hook 基礎上再包一層 Hook,拓展取數參數與返回結果。
目前還有 錯誤重試、請求超時管理、Suspense 沒有支持,看完這篇精讀後,相信你已經能夠提 PR 了。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)