国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

ahooks useRequest源碼深入解讀

3403771864 / 1115人閱讀

  大家會(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)圖:

1.png

  將 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&lt;FetchState&lt;TData, TParams&gt;&gt; = {}) {
  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 = () =&gt; {
  const [, setState] = useState({});
  return useCallback(() =&gt; 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

相關(guān)文章

  • ahooks正式發(fā)布React Hooks工具庫

      起因  社會(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...

    3403771864 評(píng)論0 收藏0
  • 常用列表頁常見hook封裝解析

      我們今天來講講關(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...

    3403771864 評(píng)論0 收藏0
  • 插件化機(jī)制"美麗"封裝你的hook請(qǐng)求使用方式解析

    我們講下 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)求  ...

    3403771864 評(píng)論0 收藏0
  • 解析ahooks整體架構(gòu)及React工具庫源碼

     這是講 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...

    3403771864 評(píng)論0 收藏0
  • 演示當(dāng)定時(shí)器在頁面最小化時(shí)無法執(zhí)行

      我們講述的是關(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ì)源碼做了一些解讀,可見詳情。  ...

    3403771864 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<