ahooks useRequest源码分析之插件,utils

129 阅读6分钟

上一篇我们阅读了useRequest 的核心代码,这一篇我们接着来看看每个插件都是在怎么实现的。
首先我们来看看plugin的TS,以方便理解后面的代码。

export type Plugin<TData, TParams extends any[]> = {
  (fetchInstance: Fetch<TData, TParams>, options: Options<TData, TParams>): PluginReturn<
    TData,
    TParams
  >; // 返回插件钩子
  onInit?: (options: Options<TData, TParams>) => Partial<FetchState<TData, TParams>>; // 返回插件初始化值
};

接下来我们来看看具体有哪些插件。

useAutoRunPlugin

这个插件主要做了什么呢。

  • 判断是否自动触发请求;

  • 在ready 为false 的时候,阻止触发请求;

  • 当refreshDeps 更改的时候自动刷新;

  • 值的注意一点的是,我们可以自己设置触发的回调,这个没有在文档中显示。如果没有设置,则直接调用 refresh 刷新。

    const useAutoRunPlugin: Plugin<any, any[]> = ( fetchInstance, { manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }, ) => { const hasAutoRun = useRef(false); hasAutoRun.current = false; // 是否准备好,并自动刷新
    useUpdateEffect(() => { if (!manual && ready) { hasAutoRun.current = true; fetchInstance.run(...defaultParams); } }, [ready]); // 依赖变化自动刷新,也可以自己设置刷新回调函数 useUpdateEffect(() => { if (hasAutoRun.current) { return; } if (!manual) { hasAutoRun.current = true; if (refreshDepsAction) { refreshDepsAction(); } else { fetchInstance.refresh(); } } }, [...refreshDeps]);

    return { onBefore: () => { // 没有准备好阻止触发请求 if (!ready) { return { stopNow: true, }; } }, }; };

useDebouncePlugin 和 useThrottlePlugin

这2个差不多,底层是通过lodashdebouncethrottle 来实现的,所以放在一起说,看一个就可以了。

const useDebouncePlugin: Plugin<any, any[]> = (
  fetchInstance,
  { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait },
) => {
  const debouncedRef = useRef<DebouncedFunc<any>>();
  const options = useMemo(() => {
    const ret: DebounceSettings = {};
    if (debounceLeading !== undefined) {
      ret.leading = debounceLeading;
    }
    if (debounceTrailing !== undefined) {
      ret.trailing = debounceTrailing;
    }
    if (debounceMaxWait !== undefined) {
      ret.maxWait = debounceMaxWait;
    }
    return ret;
  }, [debounceLeading, debounceTrailing, debounceMaxWait]);
  useEffect(() => {
    // 开启防抖  
    if (debounceWait) {
      // 这里对原来的方法做了一次拦截
      const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
      debouncedRef.current = debounce(
        (callback) => {
          callback();
        },
        debounceWait,
        options,
      );
      // debounce runAsync should be promise
      // https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
      fetchInstance.runAsync = (...args) => {
        return new Promise((resolve, reject) => {
          debouncedRef.current?.(() => {
            _originRunAsync(...args)
              .then(resolve)
              .catch(reject);
          });
        });
      };
      return () => {
        debouncedRef.current?.cancel();
        // 取消拦截
        fetchInstance.runAsync = _originRunAsync;
      };
    }
  }, [debounceWait, options]);

  if (!debounceWait) {
    return {};
  }
  return {
    // 取消防抖  
    onCancel: () => {
      debouncedRef.current?.cancel();
    },
  };
};

useLoadingDelayPlugin

通过setTimeout延迟显示loading,避免闪烁。这个代码很简单,大家看看就能明白。

const useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay }) => {
  const timerRef = useRef<Timeout>();
  if (!loadingDelay) {
    return {};
  }
  const cancelTimeout = () => {
    if (timerRef.current) {
      clearTimeout(timerRef.current);
    }
  };
  return {
    onBefore: () => {
      cancelTimeout();
      timerRef.current = setTimeout(() => {
        fetchInstance.setState({
          loading: true,
        });
      }, loadingDelay);
      return {
        loading: false,
      };
    },
    onFinally: () => {
      cancelTimeout();
    },
    onCancel: () => {
      cancelTimeout();
    },
  };
};

usePollingPlugin

通过setTimeout,按pollingInterval 时间调用refresh来轮询,其中还判断了当浏览器隐藏的时候是否轮询,这里监听了浏览器的visibilitychange 事件。

const usePollingPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { pollingInterval, pollingWhenHidden = true },
) => {
  const timerRef = useRef<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 && !isDocumentVisible()) {
        // 订阅浏览器是否可见
        unsubscribeRef.current = subscribeReVisible(() => {
          fetchInstance.refresh();
        });
        return;
      }
      timerRef.current = setTimeout(() => {
        fetchInstance.refresh();
      }, pollingInterval);
    },
    onCancel: () => {
      stopPolling();
    },
  };
};

useRefreshOnWindowFocusPlugin

当浏览器可见或是聚焦时,是否重新请求,这里监听了浏览器的visibilitychangefocus 事件。

const useRefreshOnWindowFocusPlugin: Plugin<any, any[]> = (
  fetchInstance,
  { refreshOnWindowFocus, focusTimespan = 5000 },
) => {
  const unsubscribeRef = useRef<() => void>();
  const stopSubscribe = () => {
    unsubscribeRef.current?.();
  };
  useEffect(() => {
    if (refreshOnWindowFocus) {
      // 相当于限流  
      const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan);
      // 订阅浏览器是否可见和聚焦
      unsubscribeRef.current = subscribeFocus(() => {
        limitRefresh();
      });
    }
    return () => {
      stopSubscribe();
    };
  }, [refreshOnWindowFocus, focusTimespan]);

  useUnmount(() => {
    stopSubscribe();
  });

  return {};
};

useRetryPlugin

请求失败,通过setTimeout 重试 retryCount 次,并且可以通过 retryInverval 设置重试时间间隔,最大不超过30s.

const useRetryPlugin: Plugin<any, any[]> = (fetchInstance, { retryInterval, retryCount }) => {
  const timerRef = useRef<Timeout>();
  const countRef = useRef(0);  // 重试次数
  const triggerByRetry = useRef(false); // 是否通过重试触发请求
  if (!retryCount) {
    return {};
  }
  return {
    onBefore: () => {
      // 请求之前 重置重试次数,并将之前的setTimeout移除 
      if (!triggerByRetry.current) {
        countRef.current = 0;
      }
      triggerByRetry.current = false;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
    onSuccess: () => {
      // 成功后重置重试次数
      countRef.current = 0;
    },
    onError: () => {
      // 失败一次就增加一次重试次数  
      countRef.current += 1;
      // 如果一直重试,或是重试次数小于配置的可重试次数,通过refresh 刷新请求
      if (retryCount === -1 || countRef.current <= retryCount) {
        // 如果没有设置重试间隔,则每隔 2s, 4s, 6s...递增的时间来重试刷新请求,最大间隔不大于30s
        const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.current, 30000);
        timerRef.current = setTimeout(() => {
          triggerByRetry.current = true;
          fetchInstance.refresh();
        }, timeout);
      } else {
        // 重试次数超过配置的可重试次数,则停止重试刷新请求,并重置重试次数  
        countRef.current = 0;
      }
    },
    onCancel: () => {
      // cancel 后重置重试次数,并将setTimeout移除
      countRef.current = 0;
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    },
  };
};

useCachePlugin

缓存内容包括成功请求后返回的数据,请求参数和缓存时的时间。默认会使用Map 缓存数据在内存中,下次组件初始化的时候如果有缓存数据,会先使用缓存数据渲染,然后背后请求接口成功后再使用新数据渲染。

const useCachePlugin: Plugin<any, any[]> = (
  fetchInstance,
  {
    cacheKey,
    cacheTime = 5 * 60 * 1000, // 数据缓存时间,默认 5s
    staleTime = 0, // 数据保持新鲜的时间,该时间内不会发送请求
    setCache: customSetCache, // 自定义缓存方式,可以将数据缓存到 localStorage、IndexDB 等
    getCache: customGetCache, // 通过自定义方式获取缓存数据
  },
) => {
  const unSubscribeRef = useRef<() => void>();
  const currentPromiseRef = useRef<Promise<any>>();
  // 设置缓存,2种方式,自定义的和默认的  
  const _setCache = (key: string, cachedData: CachedData) => {
    if (customSetCache) {
      customSetCache(cachedData);
    } else {
      cache.setCache(key, cacheTime, cachedData);
    }
    // 缓存的订阅
    cacheSubscribe.trigger(key, cachedData.data);
  };
  // 获取缓存, 2种方式, 自定义和默认的  
  const _getCache = (key: string, params: any[] = []) => {
    if (customGetCache) {
      return customGetCache(params);
    }
    return cache.getCache(key);
  };
  useCreation(() => {
    if (!cacheKey) {
      return;
    }
    // 开始缓存
    const cacheData = _getCache(cacheKey);
    // 有缓存数据,并且缓存的数据有data值,将重置实例的state值
    if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) {
      fetchInstance.state.data = cacheData.data;
      fetchInstance.state.params = cacheData.params;
      // 如果缓存数据可用,则loading 为false, 不会再发请求
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        fetchInstance.state.loading = false;
      }
    }
    // 订阅相同 cacheKey 的更新,从而触发更新
    unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (data) => {
      fetchInstance.setState({ data });
    });
  }, []);
  useUnmount(() => {
    // 移除订阅  
    unSubscribeRef.current?.();
  });
  if (!cacheKey) {
    return {};
  }
  return {
    onBefore: (params) => {
      // 请求之前获取缓存值
      const cacheData = _getCache(cacheKey, params);
      if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) {
        return {};
      }
      // 如果数据是新鲜的,停止请求,立即返回缓存的值
      if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
        return {
          loading: false,
          data: cacheData?.data,
          returnNow: true,
        };
      } else {
        // 如果缓存可用,返回缓存的值,但是继续请求,成功返回后的值将覆盖缓存的值
        return {
          data: cacheData?.data,
        };
      }
    },
    onRequest: (service, args) => {
      // 获取缓存的请求Promise  
      let servicePromise = cachePromise.getCachePromise(cacheKey);
      // 如果有缓存的请求Promise 并且没有被触发过,将触发这个缓存的请求Promise获取数据,不然用传进来的请求获取数据,并缓存起来
      if (servicePromise && servicePromise !== currentPromiseRef.current) {
        return { servicePromise };
      }
      servicePromise = service(...args);
      currentPromiseRef.current = servicePromise;
      cachePromise.setCachePromise(cacheKey, servicePromise);
      return { servicePromise };
    },
    onSuccess: (data, params) => {
      if (cacheKey) {
        // 移除订阅,避免自己触发
        unSubscribeRef.current?.();
        // 将成功获取的返回值缓存起来
        _setCache(cacheKey, {
          data,
          params,
          time: new Date().getTime(),
        });
        // 再次订阅
        unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
    onMutate: (data) => {
      // 更新数据和请求成功一样
      if (cacheKey) {
        // cancel subscribe, avoid trgger self
        unSubscribeRef.current?.();
        _setCache(cacheKey, {
          data,
          params: fetchInstance.state.params,
          time: new Date().getTime(),
        });
        // resubscribe
        unSubscribeRef.current = cacheSubscribe.subscribe(cacheKey, (d) => {
          fetchInstance.setState({ data: d });
        });
      }
    },
  };
};

看到这里,useRequest的插件我们就讲完了。

虽然官方文档没有说明可以自己扩展插件,但是从源码的角度来看是可以的。如果开发过程中需要我们自己扩展插件的话,也是非常的容易,其实主要注意2个地方,一个是通过onInit 返回插件的初始值从而重置Fetch 中的初始值,第二个就是返回插件的钩子函数,通过钩子函数,可以让我们在不同的请求生命周期中完成我们想要执行的逻辑,大家不妨试试~

utils 中的部分hooks

通过阅读源码,我发现里面有些工具hooks 也是非常好用,或是值得学习一下的,希望开发时碰到类似问题的时候,这些hooks能打开你的思路。

1.如果想强制刷新组件。
[, setState] = useState({});
() => setState({})

通过设置一个空对象,引用地址的变化,导致state变化,从而使组件重新渲染。
来源: useUpdate

2. 用useRef 创建复杂常量容易出现潜在性能问题。

const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了

export default function useCreation<T>(factory: () => T, deps: DependencyList) {
  const { current } = useRef({
    deps,
    obj: undefined as undefined | T
    initialized: false,
  });
  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj as T;
}

depsAreSame 怎么判断依赖是否相同呢。

export default function depsAreSame(oldDeps: DependencyList, deps: DependencyList): boolean {
  if (oldDeps === deps) return true;
  for (let i = 0; i < oldDeps.length; i++) {
    if (!Object.is(oldDeps[i], deps[i])) return false;
  }
  return true;
}

截图.png

来源: useCreation

3.判断文档是否可见
export const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';

export default function canUseDom() {
  return !!(!isUndef(window) && window.document && window.document.createElement);
}

export default function isDocumentVisible(): boolean {
  if (canUseDom()) {
    return document.visibilityState !== 'hidden';
  }
  return true;
}

来源:isDocumentVisible

4.判断浏览器是否在线
export default function isOnline(): boolean {
  if (canUseDom() && !isUndef(navigator.onLine)) {
    return navigator.onLine;
  }
  return true;
}

来源: isOnline

5.订阅屏幕聚焦变化
const listeners: any[] = [];

function subscribe(listener: () => void) {
  listeners.push(listener);
  return function unsubscribe() {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  };
}

if (canUseDom()) {
  const revalidate = () => {
    if (!isDocumentVisible() || !isOnline()) return;
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  };
  window.addEventListener('visibilitychange', revalidate, false);
  window.addEventListener('focus', revalidate, false);
}

export default subscribe;

来源: subscribeFocus
使用:

// 文档可见时添加订阅
 unsubscribeRef.current = subscribeFocus(() => {
   someListener(); // 订阅执行的代码
});

// 停止订阅
 unsubscribeRef.current?.();

到此,我们 ahooksuseRequest 的源码也就阅读完了,希望这2篇文章对你开发有帮助~