亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

ahooks?useRequest源碼精讀解析

 更新時間:2022年07月11日 11:54:16   作者:echolc55873  
這篇文章主要為大家介紹了ahooks?useRequest的源碼精讀解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前言

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

除了 React 官方提供的一些 Hooks,也支持我們能根據(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)這么多功能,我還是對其內(nèi)部的實現(xiàn)比較好奇的,所以本文就從源碼的角度帶大家了解 useRequest 的實現(xiàn)。

架構圖

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

我把 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 主要是給內(nèi)部用的 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;
}

它的定義還是比較簡單,看起來是存一個請求結果的上下文信息,這些信息其實都是需要暴露給外部用戶的,例如 loadingdata、errors 等不就是我們使用 useRequest 經(jīng)常需要拿到的數(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,這里還調(diào)用了一個 subscribe 方法,這是初始化 Fetch 類的時候傳進來的一個參數(shù),它的類型是 Subscribe,等后面將到調(diào)用的地方再看這個方法是怎么實現(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;
}

實際上都是一些回調(diào)鉤子,從名字對應上來看,對應了請求的各個階段,除了 onMutate 是其內(nèi)部擴展的一個鉤子。

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

要搞清楚 Fetch 的運作方式,我們需要看兩個核心 API 的實現(xiàn):runPluginHandlerrunAsync,其它所有的 API 實際上都在調(diào)用這兩個 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);
}

這個方法實現(xiàn)還是比較簡單,只有兩行代碼。跟我們之前猜測的大致差不多,這個方法就是接收一個 event 參數(shù),它的類型就是 keyof PluginReturn<TData, TParams>,也就是:onBefore | onRequest | onSuccess | onError | onFinally | onCancel | onMutate 的聯(lián)合類型,以及其它額外的參數(shù),然后從 pluginImpls 中找出所有對應的 event 回調(diào)鉤子函數(shù),然后執(zhí)行回調(diào)函數(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;
    }
  }

看著代碼挺多的,其實看下來很好理解。 這個函數(shù)實際上做的事就是調(diào)用我們傳入的獲取數(shù)據(jù)的方法,然后拿到成功或者失敗的結果,進行一系列的數(shù)據(jù)處理,然后更新到 state,執(zhí)行插件的各回調(diào)鉤子,還有就是我們通過 options 傳入的回調(diào)函數(shù)。

可能直接用文字直接描述比較抽象,下面我們分請求階段分析代碼。

首先前兩行是對 count 屬性的累加處理,之前我們不知道這個屬性的作用,看到這里可能猜測大概是跟請求相關的,后面看到 currentCount 的使用的地方,我們再說。

onBefore

接下來 5~27 行實際上是對 onBefore 回調(diào)鉤子的執(zhí)行,然后拿到結果做的一些邏輯處理。這里調(diào)用的就是 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 回調(diào),也就是說在調(diào)用 useRequest 的時候,我們可以通過 options 參數(shù)傳入 onBefore 函數(shù),進行請求之前的一些邏輯處理。

onRequest

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

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

onSuccess

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

onFinally

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

onError

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

其它 API

其它的例如 run、cancel、refresh 等 API,實際上調(diào)用的是 runPluginHandlerrunAsync API,例如 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 調(diào)用我們已經(jīng)很清楚它的作用了,這里值得注意的是對 count 的修改。前面我們提到每次 runAsync 一些核心階段會判斷 count 是否和 currentCount 能對得上,看到這里我們就徹底明白了 count 的作用了。實際上在我們執(zhí)行了 run 的操作,如果在本次 runAsync 方法執(zhí)行過程中,我們就調(diào)用了 cancel 方法,那么無論是在請求發(fā)起前還是后,都會把本次執(zhí)行當做 cancel 處理,返回空的數(shù)據(jù)。也就是說,這個 count 就是為了實現(xiàn)請求取消功能的一個標識。

小結

看完了 runAsync 的實現(xiàn),實際上就代表我們看完了 Fetch 的核心邏輯。從一個請求的生命周期角度來看,其實它的實現(xiàn)就很容易理解,主要做兩件事:

  • 執(zhí)行各階段的鉤子回調(diào);
  • 更新數(shù)據(jù)到 state。

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

plugins

其實看完了 Fetch,還沒看插件,你腦子里就大概知道怎么去實現(xiàn)一個插件。因為插件比較多,限于篇幅原因,這里就以 usePollingPlugin 和 useRetryPlugin 兩個插件為例,進行詳細的源碼介紹。

usePollingPlugin

首先需要清楚一點每個插件實際也是一個 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();
    },
  };
};

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

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

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

useRetryPlugin

假設讓你去設計一個 retry 的插件,那么你的設計思路是什么了?需要關注的核心邏輯是什么?還是前面那句話: 每個插件的作用就是在請求的各個階段進行定制化的邏輯處理,那如果要實現(xiàn) retry 肯定你首要關注的是,什么時候才需要 retry?答案顯而易見,那就是請求失敗的時候,也就是需要在 onError 回調(diào)實現(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 的回調(diào)中。首先前面定義了一個 countRef,記錄 retry 的次數(shù)。執(zhí)行了 onError 回調(diào),代表新的一次請求錯誤發(fā)生,然后判斷如果 retryCount 為 -1,或者當前 retry 的次數(shù)還小于用戶自定義的次數(shù),則通過一個定時器設置下次 retry 的時間,否則將 countRef 重置。

還需要注意的是其它的一些回調(diào)的處理,比如當請求成功或者被取消,需要重置 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ù),大概可以知道,內(nèi)置的一些插件應該會在更上層的地方傳進來,做一些參數(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>[]);
}

這里就會把內(nèi)置的插件傳入進去,當然還有用戶自定義的插件。實際上 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)在有個問題,因為計算預算是項目通用的邏輯。在另外一個頁面也需要這段計算邏輯,但是那個頁面已經(jīng)從后端其它的接口獲取了預算信息,或者通過其它方式構造了計算預算需要的數(shù)據(jù)。所以這里的核心矛盾點在于很多頁面依賴這段計算邏輯,但是數(shù)據(jù)來源是不一致的。將獲取預算配置和其它信息的接口邏輯放在這個 Hook 里面就會導致它的職責不單一,所以沒法很容易在其它場景復用。

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

但是對于 useRequest 這樣功能很復雜的 Hook 又怎么理解了?從功能上看,感覺它既做了一般請求數(shù)據(jù)的功能,又做了輪詢,做了緩存,做了重試,做了。。。反正很多很多的職責。

但是,如果你認真思考,發(fā)現(xiàn)這些功能又是依賴請求這個關鍵點,也就是說從這個角度來看,它們的抽象是在同一層次上。而且 useRquest 是一個更加通用的 Hook,它作為一個 package 給大量的用戶使用。如果你是一個使用者,你八成希望它是什么能力都有,你需要的它有,你暫時不需要的,它也幫你想好了。

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

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

總結

本文主要從 Fetch 類的實現(xiàn)和 plugins 的設計詳細解析了 useRequest 的源碼,看完源碼,我們知道了:

  • useRequest 核心源碼主要在 Fetch 類的實現(xiàn)中,通過巧妙的將請求劃分為各個階段的設計,然后把豐富的功能交給每個插件去實現(xiàn),解耦功能之間的關系,降低本身維護的復雜度,提高可測試性;
  • useRequest 雖然只是一個代碼千行左右的 Hook,但是通過插件化機制,使得各個功能之間完全解耦,提高了代碼的可維護性和可測試性,同時也提供了用戶自定義插件的能力;
  • 職責單一的原則在任何場景下引用都不會過時,我們在設計一些 Hook 的時候應該也要考慮單一原則。但是在設計一些跨多項目通用的 Hook,應該朝著深模塊的角度設計,提供簡單的接口,把復雜度隱藏在模塊內(nèi)部。

以上就是ahooks useRequest源碼精讀解析的詳細內(nèi)容,更多關于ahooks useRequest源碼的資料請關注腳本之家其它相關文章!

相關文章

  • 淺談React useDebounce 防抖原理

    淺談React useDebounce 防抖原理

    本文主要介紹了淺談React useDebounce 防抖原理,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2022-08-08
  • React RenderProps模式超詳細講解

    React RenderProps模式超詳細講解

    render props是指一種在 React 組件之間使用一個值為函數(shù)的 prop 共享代碼的技術。簡單來說,給一個組件傳入一個prop,這個props是一個函數(shù),函數(shù)的作用是用來告訴這個組件需要渲染什么內(nèi)容,那么這個prop就成為render prop
    2022-11-11
  • 詳解如何使用Jest測試React組件

    詳解如何使用Jest測試React組件

    在本文中,我們將了解如何使用Jest(Facebook 維護的一個測試框架)來測試我們的React組件,我們將首先了解如何在純 JavaScript 函數(shù)上使用 Jest,然后再了解它提供的一些開箱即用的功能,這些功能專門用于使測試 React 應用程序變得更容易,需要的朋友可以參考下
    2023-10-10
  • React利用scheduler思想實現(xiàn)任務的打斷與恢復

    React利用scheduler思想實現(xiàn)任務的打斷與恢復

    這篇文章主要為大家詳細介紹了React如何利用scheduler思想實現(xiàn)任務的打斷與恢復,文中的示例代碼講解詳細,感興趣的小伙伴可以參考一下
    2024-03-03
  • 淺談React中組件間抽象

    淺談React中組件間抽象

    這篇文章主要介紹了淺談React中組件間抽象,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-01-01
  • React中Redux Hooks的使用詳解

    React中Redux Hooks的使用詳解

    這篇文章主要介紹了React Redux Hooks的使用詳解,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • React?hooks?useState異步問題及解決

    React?hooks?useState異步問題及解決

    這篇文章主要介紹了React?hooks?useState異步問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-08-08
  • 使用React實現(xiàn)內(nèi)容滑動組件效果

    使用React實現(xiàn)內(nèi)容滑動組件效果

    這篇文章主要介紹了使用React實現(xiàn)一個內(nèi)容滑動組件效果,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-05-05
  • react時間分片實現(xiàn)流程詳解

    react時間分片實現(xiàn)流程詳解

    實現(xiàn)react時間分片,主要內(nèi)容包括什么是時間分片、為什么需要時間分片、實現(xiàn)分片開啟 - 固定、實現(xiàn)分片中斷、重啟 - 連續(xù)、分片重啟、實現(xiàn)延遲執(zhí)行 - 有間隔、時間分片異步執(zhí)行方案的演進、時間分片簡單實現(xiàn)、總結、基本概念、基礎應用、原理機制和需要注意的事項等
    2022-11-11
  • React中的頁面跳轉方式示例詳解

    React中的頁面跳轉方式示例詳解

    React Router提供了幾種不同的跳轉方式,包括使用組件進行頁面跳轉、使用組件進行重定向,以及使用編程式導航進行跳轉,這篇文章主要介紹了React中的頁面跳轉方式詳解,需要的朋友可以參考下
    2023-09-09

最新評論