一、了解ts基本语法
涉及ts的变量声明、接口、类、函数、泛型等
二、支持功能
前提:定义了一个 Fecth 类,用于处理请求数据。
class Fetch<R, P extends any[]> {
config: FetchConfig<R, P>;
service: Service<R, P>;
// 请求时序
count = 0;
// 是否卸载
unmountedFlag = false;
// visible 后,是否继续轮询
pollingWhenVisibleFlag = false;
pollingTimer: any = undefined;
loadingDelayTimer: any = undefined;
subscribe: Subscribe<R, P>;
unsubscribe: noop[] = [];
that: any = this;
state: FetchResult<R, P> = {
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<R, P>,
config: FetchConfig<R, P>,
subscribe: Subscribe<R, P>,
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<User>('/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<User> {
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]}" !`);
}
});
...
<button onClick={() => run(state)}>
{loading ? 'Editting...' : 'Edit'}
</button>
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]}`);
}
});
...
<ul>
<li>user A: <Button loading={fetches['A']?.loading} onClick={() => { run('A') }}>disabled</Button></li>
<li>user B: <Button loading={fetches['B']?.loading} onClick={() => { run('B') }}>disabled</Button></li>
<li>user C: <Button loading={fetches['C']?.loading} onClick={() => { run('C') }}>disabled</Button></li>
</ul>
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<R, P extends any[]> {
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 (
<div>
<p>When the mouse hovers over the link, the detail page data is preloaded.</p>
<ul>
<li><Link to="/preload/intro" onMouseEnter={() => getIntroAction.run()}>intro</Link></li>
<li><Link to="/preload/article" onMouseEnter={() => getArticleAction.run()}>article</Link></li>
</ul>
</div>
);
};
// ---------- intro.js ----------
export default () => {
const { data, loading } = useRequest(getIntro, {
cacheKey: 'intro'
});
return (
<Spin spinning={!data && loading}>
<p>Latest request time: {data?.time}</p>
<p>{data?.data}</p>
</Spin>
);
};
// ---------- article.js ----------
export default () => {
const { data, loading, ...rest } = useRequest(getArtitle, {
cacheKey: 'article'
});
return (
<Spin spinning={!data && loading}>
<p>Latest request time: {data?.time}</p>
<p>{data?.data}</p>
</Spin>
);
};
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)源码分析
- 全局监听
visibilitychange
和focus
事件 - 当屏幕聚焦时,重新调用全部需要订阅的方法
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 (
<div>
{data &&
<>
<div>userId: {data.id}</div>
<div>usrename: {data.username}</div>
</>
}
<input
onChange={e => setState(e.target.value)}
value={state}
placeholder="Please enter username"
style={{ width: 240, marginRight: 16 }}
/>
<button onClick={() => editAction.run(state)}>
{editAction.loading ? 'Editting...' : 'Edit'}
</button>
</div>
);
};
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
都必须在初次请求完成后才有意义,当调用完hook
的run
方法后,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<U, P>;
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.count
和 currentCount
出现数据不同步的情况,比如,第一次调用 _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)
}
}