useRequest 探究(二)

1,670 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情

这期主要讲useRequest这个库的用法以及源码实现,主要讲插件们的架构思路,介绍轮询、依赖更新、防抖节流、错误重试、缓存等功能和源码实现。

插件系统

useRequest核心代码简单,高级功能都是通过插件式组织代码

插件就是钩子函数的集合。讲一下怎么实现的

举个例子 : useRequest(fn,{name:'xx'})

  • ./plugins/useLoggerPlugin.js
function useLoggerPlugin(fetchInstance,{ name } ){
	return {
		onBefore(){}
        onRequest(service, params) {}
    	onSuccess() {}
		...
	}
}
//onInit是用来初始化Fetch的初始状态的,在这个时候还没有fetchInstance
useLoggerPlugin.onInit = ({ name }) => {
  console.log(name, 'onInit');
  return { name };
}
export default useLoggerPlugin;
  • useRequest.js
import useRequestImplement from './useRequestImplement';
import useLoggerPlugin from './plugins/useLoggerPlugin';
function useRequest(service, options, plugins = []) {
  return useRequestImplement(service, options,
       // plugins 是自定义的插件 ,useLoggerPlugin是上面定义的插件
    [...(plugins),
      useLoggerPlugin,
    ]
  );
}
export default useRequest;
  • useRequestImplement.js
function useRequestImplement(service, options, plugins) {
    // options : {name:'xx'} ,plugins :[useLoggerPlugin]
  const fetchOptions = { manual, ...rest };
。。。
  const fetchInstance = useCreation(() => {
      // 把 options 传给 插件们(useLoggerPlugin)初始化 得到插件初始化的数据,[name:'xx']
    const initStates = plugins.map(p => p?.onInit?.(fetchOptions)).filter(Boolean);
    //把每个插件返回的初始状态进行累加成一个最终的初始状态
    return new Fetch(serviceRef, fetchOptions, update, Object.assign({}, ...initStates));
  }, [])
  fetchInstance.options = fetchOptions;
    // 给每个插件传入参数,得到一个个的插件实例。
  fetchInstance.pluginImpls = plugins.map(p => p(fetchInstance, fetchOptions));
。。。
  • fetch.js
class Fetch {
  constructor(serviceRef, options, subscribe, initialState = {}) {
。。。//把初始化的值当成这里的默认值
    this.state = {
      loading: !options.manual, data: undefined, error: undefined,
      params: undefined, ...initialState
    };
  }
    // 执行 插件生命周期的方法。
  runPluginHandler(event, ...rest) {
    const r = this.pluginImpls.map(i => i[event]?.(...rest)).filter(Boolean);
    return Object.assign({}, ...r);
  }
  async runAsync(...params) {
      // 相当于把参数传给插件的onBefore方法去执行了
    const {  ...state } = this.runPluginHandler('onBefore', params);
      // 请求是将 请求传给插件,插件处理后,如果没有返回请求方法,就用原来的,如果变了就用新的
      let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
      if (!servicePromise) {
          servicePromise = this.serviceRef.current(...params);
      }
      const res = await servicePromise
      this.runPluginHandler('onSuccess', res, params);
      。。。这些就不赘述了,在什么时候就执行什么插件的方法即可。
      this.options.onError?.(error, params);
  }

过程

1.创建插件,插件接收两个参数,fetch实例和配置项,有个静态初始化方法初始化配置,插件最终返回一个对象,里面有各种生命周期函数

2.使用useRequest时,在创建fetch实例前,会将所有插件初始化,得到处理后的初始化数据,在创建fetch实例时当初始值,得到实例后作为参数传入插件,得到插件实例们

3.在请求启动前,执行onBefore钩子,也要把参数传入插件实例们执行,返回的值当做fetch实例的值。

onSuccess等的时候也一样,onRequest会比较特殊,传入请求方法,插件处理后,如果没有返回请求方法,就用原来的,如果变了就用新的

loading delay

ahooks.gitee.io/zh-CN/hooks…

通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。

因为请求很快,你会闪现出一个加载中,这很没必要

原理就是请求前,使用 loading 变成 false来覆盖原来的true,让他隔 x 秒后 再设 loading 变成 true

  • useLoadingDelayPlugin.js
import { useRef } from 'react';
function useLoadingDelayPlugin(fetchInstance,
  { loadingDelay }) {
  const timerRef = useRef();
  if (!loadingDelay) {
    return {};
  }
  const cancelTimeout = () => {
    if (timerRef.current)
      clearTimeout(timerRef.current);
  }
  return {
    onBefore() {//在请求前设置一个定计器,在loadingDelay时间后变成true
      timerRef.current = setTimeout(() => {
        fetchInstance.setState({ loading: true });
      }, loadingDelay);
        // const { ...state } = this.runPluginHandler('onBefore', params);
        // 这里返回false,this.setState({ loading: true, params, ...state });//fetch那里接收state并覆盖
      return { loading: false };
    },
    onFinally() {
      cancelTimeout();
    },
    onCancel() {
      cancelTimeout();
    }
  }
}
export default useLoadingDelayPlugin;
  • useRequest.js 插件的引用
import useRequestImplement from './useRequestImplement';
import useLoggerPlugin from './plugins/useLoadingDelayPlugin';
function useRequest(service, options, plugins = []) {
  return useRequestImplement(service, options,
       // plugins 是自定义的插件 ,useLoggerPlugin是上面定义的插件
    [...(plugins),
      useLoadingDelayPlugin,
    ]
  );
}
export default useRequest;

轮询

通过设置 options.pollingInterval,进入轮询模式,useRequest 会定时触发 service 执行。

原理是请求结束的时候,设置定时器去请求。

还有这个参数

pollingWhenHidden在页面隐藏时,是否继续轮询。如果设置为 false,在页面隐藏时会暂时停止轮询,页面重新显示时继续上次轮询。
  • usePollingPlugin.js
// 精髓版本
function usePollingPlugin(fetchInstance,
  { pollingInterval, pollingWhenHidden }) {  
	const timeRef = useRef();
   onFinally() {
      timeRef.current = setTimeout(() => {
        fetchInstance.refresh();
      }, pollingInterval);
    },
// 完整版
// 加了 开始前和取消后 清除定时器、当pollingInterval为false时也去掉。 
        //对 pollingWhenHidden 的支持,
import { useRef } from 'react';
import useUpdateEffect from '../../../useUpdateEffect';
import isDocumentVisible from '../utils/isDocumentVisible';
import subscribeReVisible from '../utils/subscribeReVisible';
function usePollingPlugin(fetchInstance,
  { pollingInterval, pollingWhenHidden }) {
  const timeRef = useRef();
  const unsubscribeRef = useRef();
    // 去除定时器,在开始前和结束后
  const stopPolling = () => {
    if (timeRef.current)
      clearTimeout(timeRef.current);
      // 去掉监听
    unsubscribeRef.current?.();
  }
  // pollingInterval是允许为变量的,所以要判断,为false的时候要清除定时器
  useUpdateEffect(() => {
    if (!pollingInterval) {
      stopPolling();
    }
    return () => console.log('stopPolling');
  }, [pollingInterval]);
  if (!pollingInterval) {
    return {};
  }
const isDocumentVisible = function() {
  return document.visibilityState !== 'hidden';
}
  return {
    onBefore() {
      stopPolling();
    },
    onFinally() {
      //现在不是一定要开始定时器。而是要判断pollingWhenHidden参数
      //如果设置为页面不可见的不轮询,并且页面不可见
      if (!pollingWhenHidden && !isDocumentVisible()) {
        //在此做一个订阅,订阅页面可见的事件,页面可见之后继续轮询
        unsubscribeRef.current = subscribeReVisible(() => fetchInstance.refresh());
        return;
      }
        // 请求结束后。设置定时器去请求。
      timeRef.current = setTimeout(() => {
        fetchInstance.refresh();
      }, pollingInterval);
    },
    onCancel() {
      stopPolling();
    }
  }
}
export default usePollingPlugin;
  • ../utils/subscribeReVisible
// 一个订阅器 类似redux,订阅后,返回一个可以取消订阅的函数
import isDocumentVisible from './isDocumentVisible';
const listeners = []; // 存放监听事件
function subscribe(listener) {
  listeners.push(listener);
  return function () {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  }
}
// 当焦点恢复的时候,执行里面的监听事件
function revalidate() {
  if (!isDocumentVisible()) return;
  listeners.forEach(l => l());
}
window.addEventListener('visibilitychange', revalidate);
export default subscribe;

ready

ahooks.gitee.io/zh-CN/hooks…

useRequest 提供了一个 options.ready 参数,当其值为 false 时,请求永远都不会发出。

其具体行为如下:

  1. manual=false 自动请求模式时,每次 readyfalse 变为 true 时,都会自动发起请求,会带上参数 options.defaultParams
  2. manual=true 手动请求模式时,只要 ready=false,则通过 run/runAsync 触发的请求都不会执行

场景:触发了什么条件才允许请求

  • useAutoPlugin.js
import { useUpdateEffect } from 'ahooks';
import { useRef } from 'react';
function useAutoPlugin(fetchInstance,
  { ready = true, manual, defaultParams, refreshDeps = [], refreshDepsAction }) {
  //是否已经自动运行过了
  const hasAutoRun = useRef(false);
  hasAutoRun.current = false;
    // 当 `manual=false` 自动请求模式时,每次 `ready` 从 `false` 变为 `true` 时,都会自动发起请求,会带上参数 `options.defaultParams`。
  useUpdateEffect(() => {
    if (!manual && ready) {//如果当前是自动模式,并且ready变更为true
      hasAutoRun.current = true;//设置已经自动运行过为true
      fetchInstance.run(...defaultParams);
    }
  }, [ready]);

  return {
      // 阻止请求,返回stopNow
      //当 `manual=true` 手动请求模式时,只要 `ready=false`,则通过 `run/runAsync` 触发的请求都不会执行
    onBefore() {
      if (!ready) {
        return { stopNow: true }//跳过当前的执行,先不要发请求,因为我还没准备好
      }
    }
  }
}
//用来设置初始状态state的 ready表示是否就绪,默认值是true,manual是是否是手动模式,默认是false.默认是自动模式
useAutoPlugin.onInit = ({ ready = true, manual = false }) => {
  return { loading: !manual && ready }//如果是自动模式,并且ready=true,loading为true,相当 于会自动发请求
}
export default useAutoPlugin;
  • fetch.js
  async runAsync(...params) {
	// 当返回stopNow,阻止请求
    const { stopNow, returnNow, ...state } = this.runPluginHandler('onBefore', params);
    if (stopNow) {
      return new Promise(() => { });
    }

依赖更新

useRequest 提供了一个 options.refreshDeps 参数,当它的值变化后,会重新触发请求。

const [userId, setUserId] = useState('1');
const { data, run } = useRequest(() => getUserSchool(userId), { 
    refreshDeps: [userId],
    refreshAction(){run(userId)}
});

还有个方法,refreshAction,更新的时候,有传的时候调这个,没有就走 fetch.refresh

  • useAutoPlugin.js

github1s.com/alibaba/hoo…

// 加入这块代码   监听 refreshDeps 变化 如果是自动加载的,判断有无refreshDepsAction,有传的时候调这个,没有就走 `fetch.refresh`
// 这里的hasAutoRun.current其实我也不太懂
useUpdateEffect(() => {
    if (hasAutoRun.current) {
      return;
    }
    if (!manual) {
      hasAutoRun.current = true;
      if (refreshDepsAction) {
        refreshDepsAction();
      } else {
        fetchInstance.refresh();
      }
    }
  }, [...refreshDeps]);

屏幕聚焦重新请求

ahooks.gitee.io/zh-CN/hooks…

通过设置 options.refreshOnWindowFocus,在浏览器窗口 refocusrevisible 时,会重新发起请求。

const { data } = useRequest(getUsername, {
  refreshOnWindowFocus: true,
});

和 轮询 类似,订阅 visibilitychangefocus

focusTimespan重新请求间隔,单位为毫秒number5000
  • useRefreshOnWindowFocusPlugin.js

import { useEffect, useRef } from 'react';
import subscribeFocus from '../utils/subscribeFocus';
import unUnMount from '../../../useUnmount';
import limit from '../utils/limit';
function useRefreshOnWindowFocusPlugin(fetchInstance,
  { refreshOnWindowFocus, focusTimespan }) {
  const unsubscribeRef = useRef();
  const stopSubscribe = () => {
    unsubscribeRef.current?.();
  }
  useEffect(() => {
    if (refreshOnWindowFocus) {
        // limit 用于节流,防止多次请求。
      const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan)
      unsubscribeRef.current = subscribeFocus(() => limitRefresh());
    }
    return () => {
      stopSubscribe();
    }
  }, [refreshOnWindowFocus, focusTimespan]);
  unUnMount(() => {
    stopSubscribe();
  })
  return {}
}
export default useRefreshOnWindowFocusPlugin;

--------
// 一定间隔时间内不再执行
function limit(fn, timespan) {
  let pending = false;
  return (...args) => {
    if (pending) return;
    pending = true;
    fn(...args);
    setTimeout(() => pending = false, timespan);
  }
}
export default limit;
  • subscribeFocus.js
import isDocumentVisible from './isDocumentVisible';
const listeners = [];
function subscribe(listener) {
  listeners.push(listener);
  return function () {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  }
}
function revalidate() {
  if (!isDocumentVisible()) return;
  listeners.forEach(l => l());
}
window.addEventListener('visibilitychange', revalidate);
window.addEventListener('focus', revalidate);
export default subscribe;

防抖

行为结束后一段时间再执行

通过设置 options.debounceWait,进入防抖模式,此时如果频繁触发 run 或者 runAsync,则会以防抖策略进行请求。

const { data, run } = useRequest(getUsername, {
  debounceWait: 300,
  manual: true
});
  • useDebouncePlugin.js
简单理解就是 fetchInstance.runAsync = debounce(fetchInstance.runAsync,debounceWait)
import { useEffect, useRef } from 'react';
function useDebouncePlugin(fetchInstance,
  { debounceWait }) {
  const debounceRef = useRef();
  useEffect(() => {
    if (debounceWait) {
        // 这里有点绕 
        // debounce(callback => callback(), debounceWait);中
        // fn = callback => callback()
        // 执行 runAsync ,即执行 debounceRef.current?.(() =>  originRunAsync(...args));
        // 也就是  debounce(callback => callback(), debounceWait)(() =>  originRunAsync(...args))
        // 进入定时器
        // wait后,执行callback => callback()(...args),此时args=[() => originRunAsync(...args)] 
        // (callback) => callback()(...[() => originRunAsync(...args)] )
        // callback = originRunAsync(...args)
        // (() => originRunAsync(...args))=> () => originRunAsync(...args)()
        // originRunAsync(...args)
        // 简单理解就是 debounce(callback => callback(), debounceWait)(()=>x())
        // fn = callback => callback(),fn(fn1)就是fn1(),也就是()=>x()(),就是x()
      const originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
      debounceRef.current = debounce(callback => callback(), debounceWait);
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          debounceRef.current?.(() => originRunAsync(...args).then(resolve, reject));
        });
      }
    }
  }, [debounceWait])
  return {

  }
}
function debounce(fn, wait) {
  let timer;
  return function (...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => fn(...args), wait);
  }
}
export default useDebouncePlugin;

节流

一定时间执行一次,代码和防抖类似

通过设置 options.throttleWait,进入节流模式,此时如果频繁触发 run 或者 runAsync,则会以节流策略进行请求。

const { data, run } = useRequest(getUsername, {
  throttleWait: 300,
  manual: true
});

import { useEffect, useRef } from 'react';
function useDebouncePlugin(fetchInstance,
  { throttleWait }) {
  const throttleRef = useRef();
  useEffect(() => {
    if (throttleWait) {
      const originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
      throttleRef.current = throttle(callback => callback(), throttleWait);
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          throttleRef.current?.(() => originRunAsync(...args).then(resolve, reject));
        });
      }
    }
  }, [throttleWait])
  return {

  }
}
function throttle(fn, interval) {
  let lastTime = 0;
  return function (...args) {
    const currentTime = Date.now();
    const nextTime = lastTime + interval;
    if (currentTime >= nextTime) {
      fn.apply(this, args);
      lastTime = currentTime;
    }
  }
}
export default useDebouncePlugin;

错误重试

通过设置 options.retryCount,指定错误重试次数,则 useRequest 在失败后会进行重试。

const { data, run } = useRequest(getUsername, {
  retryCount: 3,
});
retryCount错误重试次数。如果设置为 -1,则无限次重试。number-
retryInterval重试时间间隔,单位为毫秒。如果不设置,默认采用简易的指数退避算法,取 1000 * 2 ** retryCount,也就是第一次重试等待 2s,第二次重试等待 4s,以此类推,如果大于 30s,则取 30snumber-
  • useRetryPlugin.js
// 成功、启动前、取消 初始化
// 
import { useRef } from 'react';
function useRetryPlugin(fetchInstance,
  { retryCount, retryInterval }) {
  const timerRef = useRef();///定时器,什么时候重试
  const countRef = useRef();//计数器,重试的次数
  const triggerByRetry = useRef(false);//布尔值,表示此请求是否是重新请求
  if (!retryCount) {
    return {};
  }
  return {
    onBefore() {
      if (!triggerByRetry.current) {
        countRef.current = 0;
      }
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
    onSuccess() {
      countRef.current = 0;
    },
    onError() {
      countRef.current += 1;
      //如果没有到达最大的重试次数retryCount=-1重试无数次 
      if (retryCount == -1 || countRef.current <= retryCount) {
        //1s 2s 4s 8s 16s 32s
        const timeout = retryInterval || Math.min(30000, 1000 * 2 ** countRef.current)
        timerRef.current = setTimeout(() => {
          triggerByRetry.current = true;
          fetchInstance.refresh();
        }, timeout);
      } else {
        countRef.current = 0;
      }
    },
    onCancel() {
      countRef.current = 0;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    }
  }
}
export default useRetryPlugin;

缓存

如果设置了 options.cacheKeyuseRequest 会将当前请求成功的数据缓存起来。下次组件初始化时,如果有缓存数据,我们会优先返回缓存数据,然后在背后发送新请求,也就是 SWR 的能力。

你可以通过 options.staleTime 设置数据保持新鲜时间,在该时间内,我们认为数据是新鲜的,不会重新发起请求。

你也可以通过 options.cacheTime 设置数据缓存时间,超过该时间,我们会清空该条缓存数据。

SWR

在组件第二次加载时,会优先返回缓存的内容,然后在背后重新发起请求。你可以通过点击按钮来体验效果。

那就需要个缓存。

  • cache.js
const cache = new Map();
// 存数据,解构应该是为了去除引用
const setCache = (key, cachedData) => {
  cache.set(key, { ...cachedData });
}
const getCache = (key) => {
  return cache.get(key);
}

export {
  setCache,
  getCache,
}
//key='cacheKey' key=['key1','key2']
  • useCachePlugin.js

import { useRef } from 'react';
import * as cache from '../utils/cache';
function useCachePlugin(fetchInstance,
  { cacheKey, staleTime = 0 }) {

  const _getCache = (key) => {
      return cache.getCache(key);
  }
  if (!cacheKey) {
    return {}
  }
  return {
      // 请求前 在缓存中查有没有这个key对应的数据
    onBefore(params) {
      const cachedData = _getCache(cacheKey, params);
        // 找不到或者找到了这个对象但是没有data
      if (!cachedData || !Object.hasOwnProperty.call(cachedData, 'data')) {
        return {};
      } else {
        return {
          data: cachedData.data
        }
      }
    },
      // 请求成功 后 将数据缓存 
    onSuccess(data, params) {
      if (cacheKey) {
        _setCache(cacheKey, {
          data,//请求返回的数据
          params,//请求的参数
          time: new Date().getTime()//请求返回的时间
        });
      }
    }
  }
}
export default useCachePlugin;
  • fetch.js 没变
    // 这里state得到data,用缓存进行加载。
	const {  ...state } = this.runPluginHandler('onBefore', params);
    this.setState({ loading: true, params, ...state });//准备开始请求了

数据保质期

通过设置 staleTime,我们可以指定数据新鲜时间,在这个时间内,不会重新发起请求

-1表示永不过期

原理:请求时需要检查是否到时间,没到时间就直接返回数据,不要请求

  • fetch.js 请求前判断是否数据新鲜,返回returnNow,新鲜则返回 缓存数据 并 阻挡请求
  const { stopNow, returnNow, ...state } = this.runPluginHandler('onBefore', params);
    if (returnNow) {
      return Promise.resolve(state.data);
    }
  • useCachePlugin.js
function useCachePlugin(fetchInstance,
  { cacheKey, staleTime = 0,}) {    
	onBefore(params) {
      const cachedData = _getCache(cacheKey, params);
      if (!cachedData || !Object.hasOwnProperty.call(cachedData, 'data')) {
        return {};
      }
      //如果staleTime=-1表示永不过期,或者当前时间减去缓存时间小于等过保质期的话,说明在保质期内
      if (staleTime === -1 || new Date().getTime() - cachedData.time <= staleTime) {
        return {
          loading: false,
          data: cachedData.data,
          returnNow: true
        }
      } else {
        return {
          data: cachedData.data
        }
      }
    },

参数缓存

ahooks.gitee.io/zh-CN/hooks…

缓存的数据包括 dataparams,通过 params 缓存机制,我们可以记忆上一次请求的条件,并在下次初始化。

举例:
const { data, params, loading, run } = useRequest(getArticle, {
    cacheKey: 'cacheKey-demo',
  });
// params 在fetch实例中以列表存储,所以需要params[0]获取。
  const [keyword, setKeyword] = useState(params[0] || '');
  • useRequestImplement.js
  return {
    loading: fetchInstance.state.loading,
。。。
    params: fetchInstance.state.params || []

  }
  • useCachePlugin.js
// 初次挂载的时候 有没有缓存的参数。  
useCreation(() => {
      // 没有缓存key,返回
    if (!cacheKey) return;
      // 有缓存数据,给fetch实例绑定上 缓存里的参数和结果。
    const cachedData = _getCache(cacheKey);
    if (cachedData && Object.hasOwnProperty.call(cachedData, 'data')) {
      fetchInstance.state.data = cachedData.data;
      fetchInstance.state.params = cachedData.params;
        // 不过期 loading 为 false 
      if (staleTime === -1 || new Date().getTime() - cachedData.time <= staleTime) {
        fetchInstance.state.loading = false;
      }
    }
  })

删除缓存

ahooks 提供了一个 clearCache 方法,可以清除指定 cacheKey 的缓存数据。

有两种key的形式,字符串和数组。

  • cache.js
const clearCache = (key) => {
  if (key) {
      //  key=['key1','key2']
    if (Array.isArray(key)) {
      key.forEach(item => cache.delete(item));
    } else {
        //key='cacheKey'
      cache.delete(key);
    }
  } else {
      // 么有传key,就全清了
    cache.clear();
  }
}
export {
  setCache,
  getCache,
  clearCache
}
  • index.js
import { clearCache } from './src/utils/cache';
export { clearCache }

这样就能 通过 import { useRequest, clearCache} from 'ahooks';获取到了

自定义缓存

通过配置 setCachegetCache,可以自定义数据缓存,比如可以将数据存储到 localStorageIndexDB 等。

请注意:

  1. setCachegetCache 需要配套使用。
  2. 在自定义缓存模式下,cacheTimeclearCache 不会生效,请根据实际情况自行实现。
举例:
  const { data, loading } = useRequest(getArticle, {
    cacheKey,
    setCache: (data) => localStorage.setItem(cacheKey, JSON.stringify(data)),
    getCache: () => JSON.parse(localStorage.getItem(cacheKey) || '{}'),
  });

实现:

  • useCachePlugin.js
function useCachePlugin(fetchInstance,
                         //重命名下
  { cacheKey, staleTime = 0, setCache: customSetCache, getCache: customGetCache }) {
  const currentPromiseRef = useRef();
  const unSubscribeRef = useRef();
    // 判断有无 customSetCache,有就用,没有就用原本的
  const _setCache = (key, cachedData) => {
    if (customSetCache) {
      customSetCache(cachedData);
    } else {
      cache.setCache(key, cachedData);
    }
    cacheSubscribe.trigger(key, cachedData.data);
  }
  const _getCache = (key) => {
    if (customGetCache) {
      return customGetCache(key);
    } else {
      return cache.getCache(key);
    }
  }

数据共享

ahooks.gitee.io/zh-CN/hooks…

同一个 cacheKey 的内容,在全局是共享的,这会带来以下几个特性

  • 请求 Promise 共享,相同的 cacheKey 同时只会有一个在发起请求,后发起的会共用同一个请求 Promise
  • 数据同步,任何时候,当我们改变其中某个 cacheKey 的内容时,其它相同 cacheKey 的内容均会同步

实现:

要做到第一点特性,需要缓存promise

  • cachePromise.js
const cachePromise = new Map();
const setCachePromise = (key, promise) => {
  cachePromise.set(key, promise);
    // 和 cachekey类似,请求结束 后 去掉缓存
  promise.finally(() => {
    cachePromise.delete(key);
  });
}
const getCachePromise = (key) => {
  return cachePromise.get(key);
}
export {
  setCachePromise,
  getCachePromise
}
  • useCachePlugin.js
  const currentPromiseRef = useRef();
      onRequest(service, args) {
         // 第二次进来,用缓存里的promise
      let servicePromise = cachePromise.getCachePromise(cacheKey);
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return {//缓存里有,和自己的不一样,用缓存中的
          servicePromise
        };
      }
          // 第一次进,获取promise,存起来。
      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      cachePromise.setCachePromise(cacheKey, servicePromise);
      return {
        servicePromise
      }
    },

第二个特性,这里需要 订阅监听,才能做到同步。请求成功后,订阅该key更新值的功能,当数据缓存时,去触发该key下的监听,传入data,让这个 key下的data都得到更新

  • useCachePlugin.js
import * as cacheSubscribe from '../utils/cacheSubscribe';  
const unSubscribeRef = useRef();
  const _setCache = (key, cachedData) => {
   ...// 缓存 存的时候 触发 监听执行更新值的功能 
    cacheSubscribe.trigger(key, cachedData.data);
  }
    onSuccess(data, params) {
      if (cacheKey) {
        _setCache(cacheKey, {
          data,//请求返回的数据
          params,//请求的参数
          time: new Date().getTime()//请求返回的时间
        });
       // 请求成功后 订阅 更新值的功能 
        unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, d => {
          fetchInstance.setState({ data: d });
        });
      }
    }
  }
  • cacheSubscribe.js
const listeners = {};
function subscribe(key, listener) {
  if (!listeners[key]) {
    listeners[key] = []
  }
  listeners[key].push(listener);
  return function () {
    const index = listeners[key].indexOf(listener);
    listeners[key].splice(index, 1);
  }
}
// 触发 d => {fetchInstance.setState({ data: d });
function trigger(key, data) {
  if (listeners[key])
    listeners[key].forEach(listener => listener(data));
}
export {
  subscribe,
  trigger
};