umijs@use-request源码解读

一、了解ts基本语法

涉及ts的变量声明、接口、类、函数、泛型等

ts语法知识

二、支持功能

前提:定义了一个 Fecth 类,用于处理请求数据。

class Fetch {
  config: FetchConfig;
  service: Service;

  // 请求时序
  count = 0;

  // 是否卸载
  unmountedFlag = false;
  // visible 后,是否继续轮询
  pollingWhenVisibleFlag = false;

  pollingTimer: any = undefined;
  loadingDelayTimer: any = undefined;

  subscribe: Subscribe;

  unsubscribe: noop[] = [];

  that: any = this;

  state: FetchResult = {
    loading: false,
    params: [] as any,
    data: undefined,
    error: undefined,
    run: this.run.bind(this.that),
    mutate: this.mutate.bind(this.that),
    refresh: this.refresh.bind(this.that),
    cancel: this.cancel.bind(this.that),
    unmount: this.unmount.bind(this.that),
  }

  debounceRun: any;
  throttleRun: any;
  limitRefresh: any;

  constructor(
    service: Service,
    config: FetchConfig,
    subscribe: Subscribe,
    initState?: { data?: any, error?: any, params?: any, loading?: any }
  ) {...}

  // 类本身的方法,用于更新 state 并通知订阅
  setState(s = {}) {
    this.state = {
      ...this.state,
      ...s
    }
    this.subscribe(this.state);
  }

  // 实际取值函数
  _run(...args: P) {...}

  // 根据配置,分别对防抖、节流处理
  run(...args: P) {...}

  // 取消防抖、节流,清除定时器
  cancel() {...}

  // 重新请求新数据
  refresh() {...}

  // 轮询,重新调接口取值
  rePolling() {...}

  // 支持对接口返回的数据进行修改
  mutate(data: any) {...}

  // 调用cancel,并取消所有订阅
  unmount() {...}



1. 默认自动请求:在组件初次加载时自动触发请求函数,并自动管理 loading, data , error 状态。

1)用法

// 用法1:直接传入接口地址
const { data, error, loading } = useRequest('/api/userInfo')

// 用法2:传入接口调用配置
const { loading, run } = useRequest((username) => ({
    url: '/api/changeUsername',
    method: 'post',
    data: { username },
  }), {
    manual: true,
    onSuccess: (_, params) => {
      setState('');
      alert(`The username was changed to "${params[0]}" !`);
    }
  });

// 用法3:传入异步函数
import request from 'umi-request';
export async function getUserInfo(): Promise {
  return request('/api/userInfo');
}

const { data, error, loading } = useRequest(getUserInfo)

2)源码分析

第一次调用时,缓存中不存在数据,则会自动执行获取数据


// 第一次默认执行
useEffect(() => {
    if (!manual) {
      // 如果有缓存
      if (Object.keys(fetches).length > 0) {
        /* 重新执行所有的 */
        Object.values(fetches).forEach((f) => {
          f.refresh();
        });
      } else {
        // 第一次默认执行,可以通过 defaultParams 设置参数
        run(...defaultParams as any);
      }
    }
}, []);

2. 手动触发请求:设置 options.manual = true , 则手动调用 run 时才会取数

1)用法

import { changeUsername } from '@/service';

const { loading, run } = useRequest(changeUsername, {
    manual: true,
    onSuccess: (_, params) => {
      setState('');
      alert(`The username was changed to "${params[0]}" !`);
    }
});

...

2)源码分析

当开启 manual 禁止自动请求时,将 run 函数暴露给用户调用。

如果 fetchKey 不存在,则新建 Fetch 实例,保存到 feches 对象中,并调用实例的 run ,最后返回调用结果数据。
如果 fetchKey 存在,则直接调用 Fetch 实例的 run

const run = useCallback((...args: P) => {
    if (fetchKeyPersist) {
      const key = fetchKeyPersist(...args);
      newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
    }
    const currentFetchKey = newstFetchKey.current;
    // 这里必须用 fetchsRef,而不能用 fetches。
    // 否则在 reset 完,立即 run 的时候,这里拿到的 fetches 是旧的。
    let currentFetch = fetchesRef.current[currentFetchKey];
    if (!currentFetch) {
      const newFetch = new Fetch(
        servicePersist,
        config,
        subscribe.bind(null, currentFetchKey),
        {
          data: initialData
        }
      );
      currentFetch = newFetch.state;
      setFeches((s) => {
        s[currentFetchKey] = currentFetch;
        return { ...s };
      });
    }
    return currentFetch.run(...args);

  }, [fetchKey, subscribe])

3. 轮询请求:设置 options.pollingInterval 则进入轮询模式,可通过 run / cancel 开始与停止轮询

作用:在取数结束后设定 setTimeout 重新触发下一轮取数。

1)用法

const { data, loading, cancel, run } = useRequest(getRandom, {
    pollingInterval: 1000,
    pollingWhenHidden: false
});

2)源码分析

在 Fetch 类中 _run(...args: P) 的实际取值函数中,最后会判断,是否设置了轮询 pollingInterval,设置了则开启定时器。 注意,前提是当前页面没有被隐藏。

定时器及时销毁:在 _run 函数最开始,会对现有的定时器先进行销毁。

this.service(...args).then(...).finally(() => {
  if (!this.unmountedFlag && currentCount === this.count) {
    if (this.config.pollingInterval) {
      // 如果屏幕隐藏,并且 !pollingWhenHidden, 则停止轮询,并记录 flag,等 visible 时,继续轮询
      if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
        this.pollingWhenVisibleFlag = true;
        return;
      }
      this.pollingTimer = setTimeout(() => {
        this._run(...args);
      }, this.config.pollingInterval);
    }
  }
});

4. 并行请求:设置 options.fetchKey 可以对请求状态隔离,通过 fetches 拿到所有请求状态

作用:设置 options.cacheKey 后开启对请求结果缓存机制,下次请求前会优先返回缓存并在后台重新取数。

1)用法

const { run, fetches } = useRequest(disableUser, {
    manual: true,
    fetchKey: (id) => id, // 当前请求唯一标识
    onSuccess: (_, params) => {
      message.success(`Disabled user ${params[0]}`);
    }
  });

...
  • user A:
  • user B:
  • user C:

2)源码分析

每次请求都是创建一个 Fetch 实例,并用 fetchKey 进行唯一标识,并且调用 run 函数时,优先调用缓存实例。

// hooks 的 run 方法
 const run = useCallback((...args: P) => {
    if (fetchKeyPersist) {
      const key = fetchKeyPersist(...args);
      newstFetchKey.current = key === undefined ? DEFAULT_KEY : key;
    }
    const currentFetchKey = newstFetchKey.current;
    // 这里必须用 fetchsRef,而不能用 fetches。
    // 否则在 reset 完,立即 run 的时候,这里拿到的 fetches 是旧的。
    let currentFetch = fetchesRef.current[currentFetchKey];
    if (!currentFetch) {
      const newFetch = new Fetch(
        servicePersist,
        config,
        subscribe.bind(null, currentFetchKey),
        {
          data: initialData
        }
      );
      currentFetch = newFetch.state;
      setFeches((s) => {
        s[currentFetchKey] = currentFetch;
        return { ...s };
      });
    }
    return currentFetch.run(...args);

  }, [fetchKey, subscribe])

5. 请求防抖、请求节流:设置 options.debounceInterval 开启防抖,设置 options.throttleInterval 开启节流

1)用法

// 请求防抖
const { data, loading, run, cancel } = useRequest(getEmail, {
    debounceInterval: 500,
    manual: true
  });

// 请求节流
const { data, loading, run, cancel } = useRequest(getEmail, {
    throttleInterval: 500,
    manual: true
});

2)源码分析

根据传入的 config 配置来判断是否进行防抖和节流分发处理。

// 在 Fetch 类中
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';

class Fetch {
    this.debounceRun = this.config.debounceInterval ? debounce(this._run, this.config.debounceInterval) : undefined;
        this.throttleRun = this.config.throttleInterval ? throttle(this._run, this.config.throttleInterval) : undefined;

    ...

    run(...args: P) {
        if (this.debounceRun) {
          this.debounceRun(...args);
          return;
        }
        if (this.throttleRun) {
          this.throttleRun(...args);
          return;
        }
        return this._run(...args);
    }
}

6. 请求预加载:由于 options.cacheKey 全局共享,可以提前执行 run 实现预加载效果

1)用法

// --------- index.js ------------
export default () => {

  const getArticleAction = useRequest(getArtitle, {
    manual: true,
    cacheKey: 'article'
  });

  const getIntroAction = useRequest(getIntro, {
    manual: true,
    cacheKey: 'intro'
  });

  return (
    

When the mouse hovers over the link, the detail page data is preloaded.

  • getIntroAction.run()}>intro
  • getArticleAction.run()}>article
); }; // ---------- intro.js ---------- export default () => { const { data, loading } = useRequest(getIntro, { cacheKey: 'intro' }); return (

Latest request time: {data?.time}

{data?.data}

); }; // ---------- article.js ---------- export default () => { const { data, loading, ...rest } = useRequest(getArtitle, { cacheKey: 'article' }); return (

Latest request time: {data?.time}

{data?.data}

); };

2)源码分析

预加载本质是缓存机制,通过利用 useEffect 同步缓存实例, 保证缓存数据的最新,然后当需要用到数据时,优先调用缓存实例。

// cache
  useEffect(() => {
    if (cacheKey) {
      setCache(cacheKey, {
        fetches,
        newstFetchKey: newstFetchKey.current
      });
    }
  }, [cacheKey, fetches]);

7. 屏幕聚焦重新请求:设置 options.refreshOnWindowFocus = true 在浏览器 refocus 与 revisible 时重新请求

1)用法

const { data, loading } = useRequest(getUserInfo, {
    refreshOnWindowFocus: true,
    focusTimespan: 5000
});

2)源码分析

  • 全局监听 visibilitychangefocus 事件
  • 当屏幕聚焦时,重新调用全部需要订阅的方法 revalidate
  • 添加订阅时,返回取消订阅的方法(这一点很巧妙,用得好!)
// -------- subscribeFocus 方法 --------------

// from swr
import { isDocumentVisible, isOnline } from './index';

let listeners: any[] = [];

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

let eventsBinded = false;
if (typeof window !== 'undefined' && window.addEventListener && !eventsBinded) {
  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);
  // only bind the events once
  eventsBinded = true;
}

export default subscribe;

  • 设置 屏幕聚焦=true 时,订阅 limitRefresh 方法
  • limitRefresh 方法,调用了 Fetch 实例的 refresh 方法 -> Fetch 实例的 run 方法 -> Fetch 实例的 _run 方法 -> 调用接口请求数据
  • limit 限制调用的频率
  • 挂载实例 unmount 时,取消当前实例的全部订阅
// Fetch类的 constructor 中
{
    this.limitRefresh = limit(this.refresh.bind(this), this.config.focusTimespan);

    if (this.config.pollingInterval) {
      this.unsubscribe.push(subscribeVisible(this.rePolling.bind(this)));
    }
    if (this.config.refreshOnWindowFocus) {
      this.unsubscribe.push(subscribeFocus(this.limitRefresh.bind(this)));
    }
}

// Fetch类的 unmount 方法
unmount() {
    this.unmountedFlag = true;
    this.cancel();
    this.unsubscribe.forEach((s) => {
      s();
    });
}

8. 请求结果突变:可以通过 mutate 直接修改取数结果

1)用法

export default () => {
  const [state, setState] = useState('');

  const { data, mutate } = useRequest(getUserInfo, {
    onSuccess: (data) => {
      setState(data.username);
    }
  });

  const editAction = useRequest(changeUsername, {
    manual: true,
    onSuccess: (_, params) => {
      mutate((d) => ({
        ...d,
        username: params[0]
      }));
      alert(`The username was changed to "${params[0]}" !`);
    }
  });

  return (
    
{data && <>
userId: {data.id}
usrename: {data.username}
} setState(e.target.value)} value={state} placeholder="Please enter username" style={{ width: 240, marginRight: 16 }} />
); };

2)源码分析

调用 mutate 传入的方法

// Fetch 类中实现了mutate
mutate(data: any) {
    if (typeof data === 'function') {
      this.setState({
        data: data(this.state.data) || {}
      });
    } else {
      this.setState({
        data
      });
    }
}
  • cancel、refresh、mutate 都必须在初次请求完成后才有意义,当调用完 hookrun 方法后,fetches[newstFetchKey.current] 就能取到 fetch 实例,然后覆盖掉 cancel、refresh、mutate 的异常报错。
// 在 hook 中
const noReady = useCallback((name: string) => {
    return () => {
      throw new Error(`Cannot call ${name} when service not executed once.`);
    }
  }, [])

  return {
    loading: !manual,
    data: initialData,
    error: undefined,
    params: [],
    cancel: noReady('cancel'),
    refresh: noReady('refresh'),
    mutate: noReady('mutate'),

    ...(fetches[newstFetchKey.current] || {}),
    run,
    fetches,
    reset
  } as BaseResult;

9. 分页和加载更多

分页:设置 options.paginated 支持分页场景
加载更多:设置 options.loadMore 支持加载更多的情况

分页和加载原理:在 useAsync这个基础请求 hook 基础上再包一层 hook,扩展取数参数与返回结果。

所以,不在此处多余赘述了。

三、扩展知识点

1. 如何判断页面被隐藏(页面在后台标签页中 或者 浏览器最小化)

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

document.visibilityState:表示下面 4 个可能状态的值
hidden:页面在后台标签页中或者浏览器最小化
visible:页面在前台标签页中
prerender:页面在屏幕外执行预渲染处理 document.hidden 的值为 true
unloaded:页面正在从内存中卸载

visibilitychange事件:当文档从可见变为不可见或者从不可见变为可见时,会触发该事件。

2. useState 惰性初始 state

函数返回值只会在组件的初始渲染中起作用,后续渲染时会被忽略

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

3. 利用闭包保持Fetch实例最新值

分析:对于同一个实例,可能出现多次调用 _run 方法,导致 this.countcurrentCount 出现数据不同步的情况,比如,第一次调用 _run 后,刚好执行“关键点 闭包取数”后,还未执行到 return, 又执行了_run,导致此时 this.count+=1 ,那么第一次调用 _run.currentCount的值比当前的 this.count 小1。

作用:保证 state 中的数据是最近一次访问接口得到的数据


// Fetch类的实际取值函数

_run(...args: P) {
    // 取消已有定时器
    if (this.pollingTimer) {
      clearTimeout(this.pollingTimer);
    }
    // 取消 loadingDelayTimer
    if (this.loadingDelayTimer) {
      clearTimeout(this.loadingDelayTimer);
    }

    // ----------- 关键点 闭包取数 ------------

    this.count += 1;
    // 闭包存储当次请求的 count
    const currentCount = this.count;

    // ----------- 关键点 闭包取数 ------------

    this.setState({
      loading: this.config.loadingDelay ? false : true,
      params: args
    });

    if (this.config.loadingDelay) {
      this.loadingDelayTimer = setTimeout(() => {
        this.setState({
          loading: true,
        });
      }, this.config.loadingDelay);
    }

    return this.service(...args).then(res => {
      // ----------- 关键点 currentCount === this.count ------------
      if (!this.unmountedFlag && currentCount === this.count) {
        if (this.loadingDelayTimer) {
          clearTimeout(this.loadingDelayTimer);
        }
        const formattedResult = this.config.formatResult ? this.config.formatResult(res) : res;
        this.setState({
          data: formattedResult,
          error: undefined,
          loading: false
        });
        if (this.config.onSuccess) {
          this.config.onSuccess(formattedResult, args);
        }
        return formattedResult;
      }
    }).catch(error => {
      if (!this.unmountedFlag && currentCount === this.count) {
        if (this.loadingDelayTimer) {
          clearTimeout(this.loadingDelayTimer);
        }
        this.setState({
          data: undefined,
          error,
          loading: false
        });
        if (this.config.onError) {
          this.config.onError(error, args);
        }
        console.error(error);
        return error;
        // throw error;
      }
    }).finally(() => {
      if (!this.unmountedFlag && currentCount === this.count) {
        if (this.config.pollingInterval) {
          // 如果屏幕隐藏,并且 !pollingWhenHidden, 则停止轮询,并记录 flag,等 visible 时,继续轮询
          if (!isDocumentVisible() && !this.config.pollingWhenHidden) {
            this.pollingWhenVisibleFlag = true;
            return;
          }
          this.pollingTimer = setTimeout(() => {
            this._run(...args);
          }, this.config.pollingInterval);
        }
      }
    });

  }

4. useUpdateEffect : 更新才调用,初始化不调用

import { useEffect, useRef } from 'react';

const useUpdateEffect: typeof useEffect = (effect, deps) => {
  const isMounted = useRef(false);

  useEffect(() => {
    if (!isMounted.current) {
      isMounted.current = true;
    } else {
      return effect();
    }
  }, deps);
};

export default useUpdateEffect;

5. 限制函数调用次数的方法

export default function limit(fn: any, timespan: number) {
  let pending = false
  return (...args: any[]) => {
    if (pending) return
    pending = true
    fn(...args)
    setTimeout(() => (pending = false), timespan)
  }
}

参考链接

源码github地址
用法地址
精读《@umijs/use-request》源码

你可能感兴趣的:(umijs@use-request源码解读)