欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

ahooks?useRequest源碼精讀解析

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

前言

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

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

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

  • 輪詢
  • 防抖和節(jié)流
  • 錯(cuò)誤重試
  • SWR(stale-while-revalidate)
  • 緩存

等功能,基本上滿足了我們請(qǐng)求后端數(shù)據(jù)需要考慮的大多數(shù)場(chǎng)景,當(dāng)然還有 loading-delay、頁(yè)面 foucs 重新刷新數(shù)據(jù)等這些功能,但是個(gè)人理解上面列的功能才是使用比較頻繁的功能點(diǎn)。

一個(gè) Hooks 實(shí)現(xiàn)這么多功能,我還是對(duì)其內(nèi)部的實(shí)現(xiàn)比較好奇的,所以本文就從源碼的角度帶大家了解 useRequest 的實(shí)現(xiàn)。

架構(gòu)圖

我們從一張圖開(kāi)始了解其模塊設(shè)計(jì),對(duì)于一個(gè)功能復(fù)雜的 API,如果不使用合適的架構(gòu)和方式組織代碼,其擴(kuò)展性和可維護(hù)性肯定比較差。功能點(diǎn)實(shí)現(xiàn)和核心代碼混在一起,閱讀代碼的人也無(wú)從下手,也帶來(lái)更大的測(cè)試難度。雖然 useRequest 只是一個(gè) Hook,但是實(shí)際上其設(shè)計(jì)還是有清晰的架構(gòu),我們來(lái)看看 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),通過(guò)插件化機(jī)制降低了每個(gè)功能之間的耦合度,也降低了其本身的復(fù)雜度。這些點(diǎn)我們?cè)诜治鼍唧w的源碼的時(shí)候會(huì)再詳細(xì)介紹。

另外一部分核心的代碼我將其歸類為 Core(在 useRequest 的源碼中沒(méi)有這個(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ì)還是比較簡(jiǎn)潔的,而且也不是特別多,實(shí)際上有些 API 就是直接從 useRequest 暴露給外部用戶使用的,比如 run、runAsync、cancel、refresh、refreshAsync、mutate 等。像 runPluginHandler、setState 等 API 主要是給內(nèi)部用的 API,不過(guò)它也沒(méi)有做區(qū)分,從封裝的角度上來(lái)說(shuō),這一點(diǎn)個(gè)人感覺(jué)設(shè)計(jì)得不夠好。

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

那我們先來(lái)看下 FetchState<TData, TParams> 的定義,它定義在 src/type.ts 里面:

export interface FetchState<TData, TParams extends any[]> {
  loading: boolean;
  params?: TParams;
  data?: TData;
  error?: Error;
}

它的定義還是比較簡(jiǎn)單,看起來(lái)是存一個(gè)請(qǐng)求結(jié)果的上下文信息,這些信息其實(shí)都是需要暴露給外部用戶的,例如 loadingdata、errors 等不就是我們使用 useRequest 經(jīng)常需要拿到的數(shù)據(jù)信息:

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

而對(duì)應(yīng)的 Fetch 封裝了 setState API,實(shí)際上就是用來(lái)更新 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)來(lái)的一個(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)上來(lái)看,對(duì)應(yīng)了請(qǐng)求的各個(gè)階段,除了 onMutate 是其內(nèi)部擴(kuò)展的一個(gè)鉤子。

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

要搞清楚 Fetch 的運(yùn)作方式,我們需要看兩個(gè)核心 API 的實(shí)現(xiàn):runPluginHandlerrunAsync,其它所有的 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è)方法實(shí)現(xiàn)還是比較簡(jiǎn)單,只有兩行代碼。跟我們之前猜測(cè)的大致差不多,這個(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;
    }
  }

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

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

首先前兩行是對(duì) count 屬性的累加處理,之前我們不知道這個(gè)屬性的作用,看到這里可能猜測(cè)大概是跟請(qǐng)求相關(guān)的,后面看到 currentCount 的使用的地方,我們?cè)僬f(shuō)。

onBefore

接下來(lái) 5~27 行實(shí)際上是對(duì) onBefore 回調(diào)鉤子的執(zhí)行,然后拿到結(jié)果做的一些邏輯處理。這里調(diào)用的就是 runPluginHandler 方法,傳入的參數(shù)是 onBefore 和外部用戶定義的 params 參數(shù)。然后執(zhí)行完所有的 onBefore 鉤子函數(shù),拿到最后的結(jié)果,如果 stopNow 的 flag 是 true,則直接返回沒(méi)有結(jié)果的 Promise??醋⑨?,我們知道這里實(shí)際上做的是取消請(qǐng)求的處理,當(dāng)我們?cè)?onBefore 的鉤子里實(shí)現(xiàn)了取消的邏輯,符合條件后并會(huì)真正的阻斷請(qǐng)求。

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

onRequest

接下來(lái)后面的代碼就是真正執(zhí)行請(qǐng)求數(shù)據(jù)的方法了,這里就會(huì)執(zhí)行所有的 onRequest 鉤子。實(shí)際上,通過(guò) onRequest 鉤子我們是可以重寫傳入的獲取數(shù)據(jù)的方法,因?yàn)樽詈髨?zhí)行的是 onRequest 回調(diào)返回的 servicePromise。

拿到最后執(zhí)行的請(qǐng)求數(shù)據(jù)方法,就開(kāi)始發(fā)起請(qǐng)求。在這里發(fā)現(xiàn)了前面的 currentCount 的使用,它會(huì)去對(duì)比當(dāng)前最新的 count 和執(zhí)行這個(gè)方法時(shí)定義的 currentCount 是否相等,如果不相等,則會(huì)做類似于取消請(qǐng)求的處理。這里大概知道 count 的作用類似于一個(gè)”鎖“的作用,我的理解是,如果在執(zhí)行這些代碼過(guò)程有產(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)用的是 runPluginHandlerrunAsync API,例如 run:

run(...params: TParams) {
    this.runAsync(...params).catch((error) => {
      if (!this.options.onError) {
        console.error(error);
      }
    });
  }

代碼很容易看懂,就不過(guò)多介紹。

我們來(lái)看看 cancel 的實(shí)現(xiàn):

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

最后的 runPluginHandler 調(diào)用我們已經(jīng)很清楚它的作用了,這里值得注意的是對(duì) count 的修改。前面我們提到每次 runAsync 一些核心階段會(huì)判斷 count 是否和 currentCount 能對(duì)得上,看到這里我們就徹底明白了 count 的作用了。實(shí)際上在我們執(zhí)行了 run 的操作,如果在本次 runAsync 方法執(zhí)行過(guò)程中,我們就調(diào)用了 cancel 方法,那么無(wú)論是在請(qǐng)求發(fā)起前還是后,都會(huì)把本次執(zhí)行當(dāng)做 cancel 處理,返回空的數(shù)據(jù)。也就是說(shuō),這個(gè) count 就是為了實(shí)現(xiàn)請(qǐng)求取消功能的一個(gè)標(biāo)識(shí)。

小結(jié)

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

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

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

plugins

其實(shí)看完了 Fetch,還沒(méi)看插件,你腦子里就大概知道怎么去實(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)景下通過(guò)設(shè)置這個(gè) flag 停止輪詢。在真實(shí)的場(chǎng)景中,確實(shí)有比如要求用戶在切換到其它 tab 頁(yè)時(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)開(kāi)始了輪詢,在每次新的請(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?答案顯而易見(jiàn),那就是請(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 等選型,從命名上看跟我們剛開(kāi)始分析需求的時(shí)候想的差不多。

看代碼,核心的邏輯主要是在 onError 的回調(diào)中。首先前面定義了一個(gè) countRef,記錄 retry 的次數(shù)。執(zhí)行了 onError 回調(diào),代表新的一次請(qǐng)求錯(cuò)誤發(fā)生,然后判斷如果 retryCount 為 -1,或者當(dāng)前 retry 的次數(shù)還小于用戶自定義的次數(shù),則通過(guò)一個(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

分析了核心的兩塊源碼,我們來(lái)看下,怎么組裝最后的 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ù)如果使用過(guò) useRequest 的都知道,就是我們通常傳給 useRequest 的參數(shù),一個(gè)是請(qǐng)求 api,一個(gè)就是 options。這里還多了個(gè)插件參數(shù),大概可以知道,內(nèi)置的一些插件應(yīng)該會(huì)在更上層的地方傳進(jìn)來(lái),做一些參數(shù)初始化的邏輯。

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

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

原來(lái)是個(gè)”黑科技“,類似 class 組件的 $forceUpdate API,就是通過(guò) setState,讓組件強(qiáng)行渲染一次。

接著就是使用 useMount,如果發(fā)現(xiàn)用戶沒(méi)有設(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ù)雜度,也能提供更多的靈活度給到用戶,如果你覺(jué)得功能不夠,實(shí)現(xiàn)自定義插件吧。

對(duì)自定義 hook 的思考

面向?qū)ο缶幊汤锩嬗幸粋€(gè)原則叫職責(zé)單一原則, 我個(gè)人理解它的含義是我們?cè)谠O(shè)計(jì)一個(gè)類或者一個(gè)方法時(shí),它的職責(zé)應(yīng)該盡量單一。如果一個(gè)類的抽象不在一個(gè)層次,那么這個(gè)類注定會(huì)越來(lái)越膨脹,難以維護(hù)。一個(gè)方法職責(zé)越單一,它的復(fù)用性就可能越高,可測(cè)試性也越好。

其實(shí)我們?cè)谠O(shè)計(jì)一個(gè) hooks,也是需要參照這個(gè)原則的。Hooks API 出現(xiàn)的一個(gè)重大意義,就是解決我們?cè)诰帉懡M件時(shí)的邏輯復(fù)用問(wèn)題。沒(méi)有 Hooks,之前是使用 HOC、Render props或者 Mixin 等解決邏輯復(fù)用的問(wèn)題,然而每一種方式在大量實(shí)踐后都發(fā)現(xiàn)有明顯的缺點(diǎn)。所以,我們?cè)谧远x一個(gè) Hook 時(shí),總是應(yīng)該朝著提高復(fù)用性的角度出發(fā)。

光說(shuō)太抽象,舉個(gè)之前我在業(yè)務(wù)開(kāi)發(fā)中遇到的一個(gè)例子。在一個(gè)項(xiàng)目中,我們封裝了一個(gè)計(jì)算預(yù)算的 Hook 叫 useBudgetValidate,不方便貼所有代碼,下面通過(guò)偽代碼列下這個(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,
  };
}

初一看,這個(gè) Hook 沒(méi)有太大的問(wèn)題,不就是從后端獲取數(shù)據(jù),然后根據(jù)不同的傳參進(jìn)行預(yù)算計(jì)算,然后返回預(yù)算信息。但是現(xiàn)在有個(gè)問(wèn)題,因?yàn)橛?jì)算預(yù)算是項(xiàng)目通用的邏輯。在另外一個(gè)頁(yè)面也需要這段計(jì)算邏輯,但是那個(gè)頁(yè)面已經(jīng)從后端其它的接口獲取了預(yù)算信息,或者通過(guò)其它方式構(gòu)造了計(jì)算預(yù)算需要的數(shù)據(jù)。所以這里的核心矛盾點(diǎn)在于很多頁(yè)面依賴這段計(jì)算邏輯,但是數(shù)據(jù)來(lái)源是不一致的。將獲取預(yù)算配置和其它信息的接口邏輯放在這個(gè) Hook 里面就會(huì)導(dǎo)致它的職責(zé)不單一,所以沒(méi)法很容易在其它場(chǎng)景復(fù)用。

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

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

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

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

所以最后,總結(jié)下:如果我們?cè)谌粘I(yè)務(wù)開(kāi)發(fā)封裝一些 Hook,我們應(yīng)該盡量保證職責(zé)單一,以提高其復(fù)用性。如果我們需要設(shè)計(jì)一個(gè)抽象程度很高,然后給多個(gè)項(xiàng)目使用的 Hook,那么在設(shè)計(jì)的時(shí)候,應(yīng)該符合深模塊的特點(diǎn),接口盡量簡(jiǎn)單,又需要滿足各需求場(chǎng)景,將功能復(fù)雜度隱藏在 Hook 內(nèi)部。

總結(jié)

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

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

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

相關(guān)文章

  • 淺談React useDebounce 防抖原理

    淺談React useDebounce 防抖原理

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

    React RenderProps模式超詳細(xì)講解

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

    詳解如何使用Jest測(cè)試React組件

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

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

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

    淺談React中組件間抽象

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

    React中Redux Hooks的使用詳解

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

    React?hooks?useState異步問(wèn)題及解決

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

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

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

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

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

    React中的頁(yè)面跳轉(zhuǎn)方式示例詳解

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

最新評(píng)論