成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

ahooks useRequest源碼深入解讀

3403771864 / 1152人閱讀

  大家會發(fā)現(xiàn),自從 React v16.8 推出了 Hooks API,前端框架圈并開啟了新的邏輯復用的時代,從此無需在意 HOC 的無限套娃導致性能差的問題,同時也解決了 mixin 的可閱讀性差的問題。這里也有對于 React 最大的變化是函數(shù)式組件可以有自己的狀態(tài),扁平化的邏輯組織方式,更加友好地支持 TS 類型聲明。

  在運用Hooks的時候,除了 React 官方提供的,同時也支持我們能根據(jù)自己的業(yè)務場景自定義 Hooks,還有一些通用的 Hooks,例如用于請求的useRequest,用于定時器的useTimeout,用于節(jié)流的useThrottle等。于是出現(xiàn)了大量的 Hooks 庫,ahooks是其中比較受歡迎的 Hooks 庫之一,其提供了大量的 Hooks,基本滿足了大多數(shù)場景的需求。又是國人開發(fā),中文文檔友好,在我們團隊的一些項目中就使用了 ahooks。

  其中最常用的 hooks 就是useRequest,用于從后端請求數(shù)據(jù)的業(yè)務場景,除了簡單的數(shù)據(jù)請求,它還支持:

  輪詢

  防抖和節(jié)流

  錯誤重試

  SWR(stale-while-revalidate)

  緩存

  等功能,這樣看起來是不是基本上滿足了我們請求后端數(shù)據(jù)需要考慮的大多數(shù)場景,其中還有 loading-delay、頁面 foucs 重新刷新數(shù)據(jù)等這些功能,就個人看法上面的功能才是使用比較頻繁的功能點。

  一個 Hooks 實現(xiàn)這么多功能,不禁感嘆它的強大,所以本文就從源碼的角度帶大家了解 useRequest 的實現(xiàn)。

  架構圖

  下面是關于了解其模塊設計,對于一個功能復雜的 API,如果不使用合適的架構和方式組織代碼,其擴展性和可維護性肯定比較差。功能點實現(xiàn)和核心代碼混在一起,閱讀代碼的人也無從下手,也帶來更大的測試難度。雖然 useRequest 只是一個 Hook,但是實際上其設計還是有清晰的架構,我們來看看 useRequest 的架構圖:

1.png

  將 useRequest 的模塊劃分為三大塊:Core、Plugins、utils,然后 useRequest 將這些模塊組合在一起實現(xiàn)核心功能。

  先看插件部分,看到每個插件的命名,如果了解 useRequest 的功能就會發(fā)現(xiàn),基本上每個功能點對應一個插件。這也是 useRequest 設計比較巧妙的一點,通過插件化機制降低了每個功能之間的耦合度,也降低了其本身的復雜度。這些點我們在分析具體的源碼的時候會再詳細介紹。

  另外一部分核心的代碼我將其歸類為 Core(在 useRequest 的源碼中沒有這個名詞),主要實現(xiàn)了一個 Fetch 類,這個類是 useRequest 的插件化機制實現(xiàn)和其它功能的核心實現(xiàn)。

  下面我們深入源碼,看下其實現(xiàn)原理。

  源碼解析

  先看 Core 部分的源碼,主要是 Fetch 這個類的實現(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 的設計特點就是簡潔,實際上有些 API 就是直接從 useRequest 暴露給外部用戶使用的,比如 run、runAsync、cancel、refresh、refreshAsync、mutate 等。像 runPluginHandler、setState 等 API 主要是給內部用的 API,然它也有區(qū)分的做法,但從封裝的來說設計感并不好。

  重點關注下幾個 Fetch 類的屬性,一個是 state,它的類型是FetchState<TData, TParams>,一個是 pluginImpls,它是PluginReturn<TData, TParams>數(shù)組,實際上這個屬性就用來存所有插件執(zhí)行后返回的結果。還有一個 count 屬性,是number類型,這個不看源代碼,是無法知道做什么的。這點useRequest 開發(fā)者做的不夠好。注釋也很少,全靠閱讀者深入到源碼,去看使用的地方,才能知道一些方法和屬性的作用。

  那我們先來看下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 經常需要拿到的數(shù)據(jù)信息:

  const { data, error, loading } = useRequest(service);

  而對應的 Fetch 封裝了 setState API,實際上就是用來更新 state 的數(shù)據(jù): 

 setState(s: Partial&lt;FetchState&lt;TData, TParams&gt;&gt; = {}) {
  this.state = {
  ...this.state,
  ...s,
  };
  // ? 未知
  this.subscribe();
  }

  除了更新 state,這里還調用了一個 subscribe 方法,這是初始化 Fetch 類的時候傳進來的一個參數(shù),它的類型是Subscribe,等后面將到調用的地方再看這個方法是怎么實現(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;
  }

  上面其實很簡單,就都是一些回調鉤子,從名字對應上來看,對應了請求的各個階段,除了onMutate是其內部擴展的一個鉤子。

  也就是說 pluginImpls 里面存的是一堆含有各個鉤子函數(shù)的對象集合,如果技術敏銳的同學,可能很容易就想到發(fā)布訂閱模式,這不就是存了一系列的 subscribe 回調,這不過這是一個回調的集合,里面有各種不同請求階段的回調。那么到底是不是這樣,我們繼續(xù)往下看。

  要搞清楚 Fetch 的運作方式,我們需要看兩個核心 API 的實現(xiàn):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 參數(shù),它的類型就是keyof PluginReturn<TData, TParams>,也就是:onBefore | onRequest | onSuccess | onError | onFinally | onCancel | onMutate的聯(lián)合類型,以及其它額外的參數(shù),然后從 pluginImpls 中找出所有對應的 event 回調鉤子函數(shù),然后執(zhí)行回調函數(shù),拿到結果并返回。

  再看runAsync的實現(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)在我們先說下上面代碼,這個函數(shù)實際上做的事就是調用我們傳入的獲取數(shù)據(jù)的方法,然后拿到成功或者失敗的結果,進行一系列的數(shù)據(jù)處理,然后更新到 state,執(zhí)行插件的各回調鉤子,還有就是我們通過 options 傳入的回調函數(shù)。

  這樣說文字不知道大家是否聽的懂,現(xiàn)在我分請求階段分析代碼。

  首先前兩行是對 count 屬性的累加處理,詳細不在這里說,等后面看到 currentCount 的使用的地方,我們再說。

  onBefore

  接下來 5~27 行實際上是對 onBefore 回調鉤子的執(zhí)行,這樣就可以拿到結果做的一些邏輯處理。這里調用的就是 runPluginHandler 方法,傳入的參數(shù)是 onBefore 和外部用戶定義的 params 參數(shù)。然后執(zhí)行完所有的 onBefore 鉤子函數(shù),拿到最后的結果,如果 stopNow 的 flag 是 true,則直接返回沒有結果的 Promise。看注釋,我們知道這里實際上做的是取消請求的處理,當我們在 onBefore 的鉤子里實現(xiàn)了取消的邏輯,符合條件后并會真正的阻斷請求。

  當然如果沒有取消,然后接著更新 state 數(shù)據(jù),如果立即返回的 returnNow flag 為 true,則立馬將更新后的 state 返回,否則執(zhí)行用戶傳入的 options 中的 onBefore 回調,也就是說在調用 useRequest 的時候,我們可以通過 options 參數(shù)傳入 onBefore 函數(shù),進行請求之前的一些邏輯處理。

  onRequest

  現(xiàn)在就是真正執(zhí)行請求數(shù)據(jù)的方法了,這里就會執(zhí)行所有的 onRequest 鉤子。實際上,通過 onRequest 鉤子我們是可以重寫傳入的獲取數(shù)據(jù)的方法,因為最后執(zhí)行的是 onRequest 回調返回的servicePromise。

  拿到最后執(zhí)行的請求數(shù)據(jù)方法,就開始發(fā)起請求。在這里發(fā)現(xiàn)了前面的 currentCount 的使用,它會去對比當前最新的 count 和執(zhí)行這個方法時定義的 currentCount 是否相等,如果不相等,則會做類似于取消請求的處理。這里大概知道 count 的作用類似于一個”鎖“的作用,我的理解是,如果在執(zhí)行這些代碼過程有產生一些比這里優(yōu)先級更高的處理邏輯或者請求操作,是需要 cancel 掉這次的請求,以最新的請求為準。當然,最后還是要看哪些地方可能會修改 count。

  onSuccess

  執(zhí)行完請求后,如果請求成功,則拿到請求返回的數(shù)據(jù),更新到 state,執(zhí)行用戶傳入的成功回調和各插件的成功回調鉤子。

  onFinally

  成功之后,執(zhí)行 onFinally 鉤子,這里也很嚴謹,也會比較 count 的值,確保一致之后,才會執(zhí)行各插件的回調鉤子,預發(fā)一些”競態(tài)“情況的發(fā)生。

  onError

  如果請求失敗,就會進入到 catch 分支,執(zhí)行一些處理錯誤的邏輯,更新 error 信息到 state 中。同樣這里也會有 count 的對比,然后執(zhí)行 onError 的回調。執(zhí)行完 onError 也會同樣執(zhí)行 onFinally 的回調,因為一個請求要么成功,要么失敗,都會需要執(zhí)行最后的 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 的實現(xiàn):

  cancel() {
  this.count += 1;
  this.setState({
  loading: false,
  });
  this.runPluginHandler('onCancel');
  }

  最后的 runPluginHandler 調用的作用我們十分明白,要注意的是對 count 的修改。前面我們提到每次 runAsync 一些核心階段會判斷 count 是否和 currentCount 能對得上,看到這里我們就徹底明白了 count 的作用了。實際上在我們執(zhí)行了 run 的操作,如果在本次 runAsync 方法執(zhí)行過程中,我們就調用了 cancel 方法,那么無論是在請求發(fā)起前還是后,都會把本次執(zhí)行當做 cancel 處理,返回空的數(shù)據(jù)。也就是說,這個 count 就是為了實現(xiàn)請求取消功能的一個標識。

  小結

  其實這里了解runAsync的實現(xiàn),實際基本上整個的 Fetch 的核心邏輯也看的清楚。從一個請求的生命周期角度來看,這里主要做兩件事:

  執(zhí)行各階段的鉤子回調;

  更新數(shù)據(jù)到 state。

  其實這都歸功于 useRequest 的巧妙設計,我們看這部分源碼,只要看懂了類型和兩個核心的方法,都不用關心具體每個插件的實現(xiàn)。它將每個功能點的復雜度和核心的邏輯通過插件機制隔離開來,從而每個插件只需要按一定的契約實現(xiàn)好自己的功能就行,然后 Fetch 不管有多少插件,只負責在合適的時間點調用插件鉤子,做到了完全的解耦。

  plugins

  其實看完了 Fetch,還沒看插件,你腦子里就大概知道怎么去實現(xiàn)一個插件。因為插件比較多,限于篇幅原因,這里就以 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();
  },
  };
  };

  它接受兩個參數(shù),一個是 fetchInstance,也就是前面提到的 Fetch 實例,第二個參數(shù)是 options,支持傳入 pollingInterval、pollingWhenHidden 兩個屬性。這兩個屬性從命名上比較容易理解,一個就是輪詢的時間間隔,另外一個猜測應該是可以在某種場景下通過設置這個 flag 停止輪詢。在真實的場景中,確實有比如要求用戶在切換到其它 tab 頁時停止輪詢等這樣的需求。所以這個配置,還比較好理解。

  而每個插件的作用就是在請求的各個階段進行定制化的邏輯處理,以輪詢?yōu)槔?,其最核心的邏輯在?onFinally 的回調,在每次請求結束后,設置一個 setTimeout,然后按用戶傳入的 pollingInterval 進行定時執(zhí)行 Fetch 的 refresh 方法。

  還有就是停止輪詢的時機,每次用戶主動取消請求,在 onCancel 的回調停止輪詢。如果已經開始了輪詢,在每次新的請求調用的時候先停止上一次的輪詢,避免重復。當然包括,如果組件修改了 pollingInterval 等的時候,需要先停止掉之前的輪詢。

  useRetryPlugin

  假設讓你去設計一個 retry 的插件,那么你的設計思路是什么了?需要關注的核心邏輯是什么?還是前面那句話: 每個插件的作用就是在請求的各個階段進行定制化的邏輯處理,那如果要實現(xiàn) retry 肯定你首要關注的是,什么時候才需要 retry?答案顯而易見,那就是請求失敗的時候,也就是需要在 onError 回調實現(xiàn) retry 的邏輯??紤]得周全一點,你還需要知道 retry 的次數(shù),因為第二次也可能失敗了。當然還有就是 retry 的時間間隔,失敗后多久 retry?這些是外部使用者關心的,所以應該將它們設計成配置項。

  分析好了需求,我們看下 retry 插件的實現(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ù)補償
  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);
  }
  },
  };
  };

  第一個參數(shù)跟 usePollingPlugin 的插件一樣,都是接收 Fetch 實例,第二個參數(shù)是 options,支持 retryInterval、retryCount 等選型,從命名上看跟我們剛開始分析需求的時候想的差不多。

  看代碼,核心的邏輯主要是在 onError 的回調中。首先前面定義了一個 countRef,記錄 retry 的次數(shù)。執(zhí)行了 onError 回調,代表新的一次請求錯誤發(fā)生,然后判斷如果 retryCount 為 -1,或者當前 retry 的次數(shù)還小于用戶自定義的次數(shù),則通過一個定時器設置下次 retry 的時間,否則將 countRef 重置。

  還需要注意的是其它的一些回調的處理,比如當請求成功或者被取消,需要重置 countRef,取消的時候還需要清理可能存在的下一次 retry 的定時器。

  這里 onBefore 的邏輯處理怎么理解了?首先這里會有一個 triggerByRetry 的 flag,如果 flag 是 false。則會清空 countRef。然后會將 triggerByRetry 設置為 false,然后清理掉上一次可能存在的 retry 定時器。我個人的理解是這里設置一個 flag 是為了避免如果 useRequest 重新執(zhí)行,導致請求重新發(fā)起,那么在 onBefore 的時候需要做一些重置處理,以防和上一次的 retry 定時器撞車。

  小結

  其它插件的設計思路是類似的,關鍵是要分析出你需要實現(xiàn)的功能是作用在請求的哪個階段,那么就需要在這個鉤子里實現(xiàn)核心的邏輯處理。然后再考慮其它鉤子的一些重置處理,取消處理等,所以在優(yōu)秀合理的設計下實現(xiàn)某個功能它的成本是很低的,而且也不需要關心其它插件的邏輯,這樣每個插件也是可以獨立測試的。

  useRequest

  分析了核心的兩塊源碼,我們來看下,怎么組裝最后的 useRequest。首先在 useRequest 之前,還有一層抽象叫 useRequestImplement,看下是怎么實現(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í)行每個插件 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>;
  }

  前面兩個參數(shù)如果使用過 useRequest 的都知道,就是我們通常傳給 useRequest 的參數(shù),一個是請求 api,一個就是 options。這里還多了個插件參數(shù),大概可以知道,內置的一些插件應該會在更上層的地方傳進來,做一些參數(shù)初始化的邏輯。

  然后通過 useLatest 構造一個 serviceRef,保證能拿到最新的 service。接下來,使用 useUpdate Hook 創(chuàng)建了update 方法,然后再創(chuàng)建 fetchInstance 的時候作為第三個參數(shù)傳遞給 Fetch,這里就是我們前面提到過的 subscribe。那我們要看下 useUpdate 做了什么:

 

 const useUpdate = () =&gt; {
  const [, setState] = useState({});
  return useCallback(() =&gt; setState({}), []);
  };

  原來是個”黑科技“,類似 class 組件的 $forceUpdate API,就是通過 setState,讓組件強行渲染一次。

  接著就是使用 useMount,如果發(fā)現(xiàn)用戶沒有設置 manual 或者將其設置為 false,立馬會執(zhí)行一次請求。當組件被銷毀的時候,在 useUnMount 中進行請求的取消。最后返回暴露給用戶的數(shù)據(jù)和 API。

  最后看下 useRequest 的實現(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>[]);
  }

  這里就會把內置的插件傳入進去,當然還有用戶自定義的插件。實際上 useRequest 是支持用戶自定義插件的,這又突出了插件化設計的必要性。除了能降低本身自己的功能之間的復雜度,也能提供更多的靈活度給到用戶,如果你覺得功能不夠,實現(xiàn)自定義插件吧。

  對自定義 hook 的思考

  面向對象編程里面有一個原則叫職責單一原則, 我個人理解它的含義是我們在設計一個類或者一個方法時,它的職責應該盡量單一。如果一個類的抽象不在一個層次,那么這個類注定會越來越膨脹,難以維護。一個方法職責越單一,它的復用性就可能越高,可測試性也越好。

  其實我們在設計一個 hooks,也是需要參照這個原則的。Hooks API 出現(xiàn)的一個重大意義,就是解決我們在編寫組件時的邏輯復用問題。沒有 Hooks,之前是使用 HOC、Render props或者 Mixin 等解決邏輯復用的問題,然而每一種方式在大量實踐后都發(fā)現(xiàn)有明顯的缺點。所以,我們在自定義一個 Hook 時,總是應該朝著提高復用性的角度出發(fā)。

  光說太抽象,舉個之前我在業(yè)務開發(fā)中遇到的一個例子。在一個項目中,我們封裝了一個計算預算的 Hook 叫useBudgetValidate,不方便貼所有代碼,下面通過偽代碼列下這個 Hook 做的事:

 

 export default function useBudgetValidate({ id, dailyBudgetType, mode }: Options) {
  const [dailyBudgetSetting, setDailyBudgetSetting] = useState<BudgetSetting | null>(null);
  // 從后端獲取某個數(shù)據(jù)
  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 就是從后端獲取數(shù)據(jù),然后根據(jù)不同的傳參進行預算計算,然后返回預算信息。可現(xiàn)在有個問題影響,因為計算預算是項目通用的邏輯。在另外一個頁面也需要這段計算邏輯,但是那個頁面已經從后端其它的接口獲取了預算信息,或者通過其它方式構造了計算預算需要的數(shù)據(jù)。因此核心矛盾點在于很多頁面依賴這段計算邏輯,但是數(shù)據(jù)來源是不一致的。將獲取預算配置和其它信息的接口邏輯放在這個 Hook 里面就會導致它的職責不單一,所以沒法很容易在其它場景復用。

  現(xiàn)在就說說重構的思路,就是將數(shù)據(jù)請求的邏輯抽離,多帶帶封裝一個 Hook,或者把職責交給組件去做。這個 Hook 只做一件事,那就是接收配置和其它參數(shù),進行預算計算,將結果返回給外面。

  現(xiàn)在有個復雜又難解的就是useRequest的功能Hook,從功能上看,感覺它既做了一般請求數(shù)據(jù)的功能,但同時又做了輪詢,做了緩存,做了重試,做了。。。簡而言之就是很多的職責。

  但他們都依賴請求這個關鍵點,這也就表明它們的抽象是在同一層次上。而且 useRquest 是一個更加通用的 Hook,它作為一個 package 給大量的用戶使用。如果你是一個使用者,你想要什么能力,它就可以實現(xiàn)什么,超級爽。

  在Philosophy of Software Design一書中提到一個概念叫:深模塊,它的意思是:深模塊是那些既提供了強大功能但又有著簡單接口的模塊。在設計一些模塊或者 API 的時候,比如像 useRequest 這種,那么就要符合這個原則,用戶只需要少量的配置,就能使用各插件帶來的豐富功能。

  所以最后,總結下:如果我們在日常業(yè)務開發(fā)封裝一些 Hook,要記住應該盡量保證職責單一,以提高其復用性。如果我們需要設計一個抽象程度很高,然后給多個項目使用的 Hook,那么在設計的時候,應該符合深模塊的特點,接口盡量簡單,又需要滿足各需求場景,將功能復雜度隱藏在 Hook 內部。

  總結

  我們現(xiàn)在降的就是從 Fetch 類的實現(xiàn)和 plugins 的設計詳細解析了 useRequest 的源碼。

  useRequest 核心源碼主要在 Fetch 類的實現(xiàn)中,主要是通過巧妙的將請求劃分為各個階段的設計,之后將豐富的功能交給每個插件去實現(xiàn),解耦功能之間的關系,降低本身維護的復雜度,提高可測試性;

  useRequest 雖然只是一個代碼千行左右的 Hook,但是通過插件化機制,使得各個功能之間完全解耦,提高了代碼的可維護性和可測試性,同時也提供了用戶自定義插件的能力;

  職責單一的原則在任何場景下引用都不會過時,我們在設計一些 Hook 的時候應該也要考慮單一原則。但是在設計一些跨多項目通用的 Hook,應該朝著深模塊的角度設計,提供簡單的接口,把復雜度隱藏在模塊內部。

     知識點都已講述了,只看每個人自己的理解。



文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉載請注明本文地址:http://systransis.cn/yun/128265.html

相關文章

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

      起因  社會在不斷的向前,技術也在不斷的完善進步。從 React Hooks 正式發(fā)布到現(xiàn)在,越來越多的項目正在使用 Function Component 替代 Class Component,Hooks 這一新特性也逐漸被廣泛的使用。 這樣的解析是不是很熟悉,在日常中時常都有用到,但也有一個可以解決這樣重復的就是對數(shù)據(jù)請求的邏輯處理,對防抖節(jié)流的邏輯處理等。 另一方面,由于 Hoo...

    3403771864 評論0 收藏0
  • 常用列表頁常見hook封裝解析

      我們今天來講講關于ahooks 源碼,我們目標主要有以下幾點:  深入了解 React hooks?! ∶靼兹绾纬橄笞远x hooks,且可以構建屬于自己的 React hooks 工具庫?! ⌒〗ㄗh:培養(yǎng)閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。  列表頁常見元素  后臺管理系統(tǒng)中常見典型列表頁包括篩選表單項、Table表格、Pagination分頁這三部分?! ♂槍κ褂?Ant...

    3403771864 評論0 收藏0
  • 插件化機制"美麗"封裝你的hook請求使用方式解析

    我們講下 ahooks 的核心 hook —— useRequest?! seRequest 簡介  根據(jù)官方文檔的介紹,useRequest 是一個強大的異步數(shù)據(jù)管理的 Hooks,React 項目中的網絡請求場景使用 useRequest ,這就可以?! seRequest通過插件式組織代碼,核心代碼極其簡單,并且可以很方便的擴展出更高級的功能。目前已有能力包括:  自動請求/手動請求  ...

    3403771864 評論0 收藏0
  • 解析ahooks整體架構及React工具庫源碼

     這是講 ahooks 源碼的第一篇文章,簡要就是以下幾點:  加深對 React hooks 的理解?! W習如何抽象自定義 hooks。構建屬于自己的 React hooks 工具庫?! ∨囵B(yǎng)閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇?! ∽ⅲ罕鞠盗袑?ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見詳情?! 〉谝黄饕榻B a...

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

      我們講述的是關于 ahooks 源碼系列文章的第七篇,總結主要講述下面幾點:  鞏固 React hooks 的理解。  學習如何抽象自定義 hooks。構建屬于自己的 React hooks 工具庫?! ∨囵B(yǎng)閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。  注:本系列對 ahooks 的源碼解析是基于v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見詳情?! ?..

    3403771864 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<