useRequest的拓展功能
上一篇文章我们介绍了useRequest()的基本用法,这篇文章我们将讲述,useRequest是如何进行拓展的,如果我们自己以后封装hooks
的时候,应该怎么在基本功能上拓展。
拓展功能
- 延时展示loading
- 轮询请求
- 延时请求
- ready状态之前请求不触发
- 依赖刷新
useRequest拓展功能的机制
useRequest()的拓展功能都是通过类似于插件的机制,在基本功能的情况下,通过一定的钩子函数来覆盖参数进行操作。每一个插件都是一个函数,本身可以挂载一个
init()函数,也可以调用函数返回一些钩子,在请求不同的时期进行调用。
function useXXX(fetchInstance, options) {}
useXXX.onInit = (options) => {}
插件的钩子
onInit()
从上一篇文章中,我们知道useRequest的核心内容都是在Fetch中, onInit()方法主要是修改Fetch的初始化的state, 它在Fetch实例化
之前的时候调用
const fetchInstance = useCreation(() => {
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
return new Fetch(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState),
);
}, []);
onBefore()
onBefore() 会在请求之前被调用,用于修改请求发送之前的状态
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
onFinally()
onFinally() 会在请求结束后被调用,用于在请求后做一些清理工作
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
onCancel()
onCancel() 会在点击调用取消的时候执行
class Fetch {
cancel() {
this.count += 1;
this.setState({
loading: false,
});
this.runPluginHandler('onCancel');
}
}
延时展示loading
在业务中,正常的情况下,我们发送请求的时候,需要展示给用户一个加载中的状态。但是,当我们请求时间比较短的时候,就会有场景不需要展示给用户加载中的状态, 直接展示请求后的数据状态,这样就可以防止从
加载中变成请求后的闪烁,useRequest()通过配置options的loadingDelay参数,来控制多长时间我们需要展示loading状态(返回的loading为true)
const { loading, data } = useRequest(getUsername, {
loadingDelay: 300
});
return <div>{ loading ? 'Loading...' : data }</div>
内部实现(loadingDelay)
主要是通过onBefore()钩子,在请求发生前的loadingDelay时间内,将loading的状态设置为false, 不展示在请求中的状态,过了loadingDelay时间后,将
loading的状态设置为true。
我们知道插件是一个函数, 接受2个参数,一个是Fetch实例,一个是options选项。由于在请求之前会执行每一个插件返回的onBefore, 默认情况,在执行请求的之前,会将loading
设置为true, 所以useLoadingDelayPlugin的主要作用,就是在loadingDelay之前,将loading返回为false,覆盖掉本来的值。
const useLoadingDelayPlugin = (fetchInstance, {loadingDelay}) => {
const timerRef = useRef()
if(!loadingDelay) {
return {}
}
const cancelTimeout = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
return {
onBefore: (p) => {
cancelTimeout();
timerRef.current = setTimeout(() => {
fetchInstance.setState({
loading: true,
});
}, loadingDelay);
return {
loading: false,
};
},
onFinally: () => {
cancelTimeout();
},
onCancel: () => {
cancelTimeout();
},
};
}
Fetch中覆盖掉原本的loading值:
const {
...state
} = this.runPluginHandler('onBefore', params);
this.setState({
loading: true,
...state,
});
轮询请求(pollingInterval)
在实际的业务中,我们可能会有一些场景需要轮询某一个接口请求,useRequest()通过传递参数pollingInterval来指定轮询时间,我们可以通过cancel来停止轮询。
const { data, run, cancel } = useRequest(getUsername, {
pollingInterval: 3000, // 轮询时间
pollingWhenHidden: true // 页面隐藏的时候是否轮询
});
内部实现
轮询的内部实现主要是通过onBefore()和onFinally()、onCancel()来实现。
主要思路是,在每次请求结束后,新增一个定时器,在指定pollingInterval时间后,调用Fetch实例的refresh重新请求。每一次请求之前,清空上一次请求的定时器和订阅
const usePollingPlugin = ( fetchInstance,
{ pollingInterval, pollingWhenHidden = true },
) => {
const timerRef = useRef();
const unsubscribeRef = useRef();
const stopPolling = () => {
// 如果存在定时器,就清掉原来的定时器
if (timerRef.current) {
clearTimeout(timerRef.current);
}
// 删除lister的函数
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();
},
};
};
等待请求(ready)和依赖刷新(refreshDeps)
有时候,我们需要等待一些准备工作,才去发送请求, 或者当一些数据发生变化后,重新的发送请求。useRequest()提供ready参数,让我们自动去控制请求的时机、refreshDeps
参数当依赖数据发送变化后,重新发送请求。
const [ready, { toggle }] = useToggle(false);
const { data, run } = useRequest(() => getUserSchool(userId), {
refreshDeps: [userId],
ready
});
内部实现
默认情况下,useRequest()不需要等待时间,在manual为true的时候,自动发送请求,那如果我们当ready为false的时候,不发送请求,就需要在
执行请求之前,提前返回。
const useAutoRunPlugin = (fetchInstance, {manual, ready, defaultParams }) => {
const hasAutoRun = useRef(false)
// 每次运行都将其设置为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
fetchInstance.refresh();
}
}, [...refreshDeps])
return {
onBefore: () => {
if(!ready) {
return { stopNow: true }
}
}
}
}
// 在初始化的时候,根据ready的值,是否展示loading状态
useAutoRunPlugin.init = (ready = true, manual) => {
return {
loading: !manual && ready,
};
}
从上面的代码中,我们可以看出,useAutoRunPlugin插件主要是,当ready为true 的时候,重新执行了实例的run()方法,在onBefore阶段, 如果ready为false
的时候,返回stopNow为true。 而在Fetch实例的时候,当stopNow为true的时候,请求会立即返回,这样就可以做到ready为false的时候,不发出请求
class Fetch {
async runAsync(...params) {
this.count += 1;
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
}
}
防抖和节流(debounceWait和throttleWait)
useRequest()中的节流和防抖都是通过lodash中对应的库,通过对Fetch实例的runAsync进行包装,从而实现防抖和节流
const { data, run } = useRequest(getUsername, {
throttleWait: 300,
manual: true
});
内部实现
从上一篇文章中,我们可以看出useRequest的请求都是通过Fetch实例的runAsync方法,所以防抖和节流都是基于runAsync进行的改装。
import throttle from 'lodash/throttle';
const useThrottlePlugin = (
fetchInstance,
{ throttleWait, throttleLeading, throttleTrailing },
) => {
const throttledRef = useRef();
const options = {};
// 请求的相关配置
if (throttleLeading !== undefined) {
options.leading = throttleLeading;
}
if (throttleTrailing !== undefined) {
options.trailing = throttleTrailing;
}
useEffect(() => {
if (throttleWait) {
// 保留原有的发送请求方法
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance);
// 调用lodash的节流函数,记录返回的新函数
throttledRef.current = throttle(
(callback) => {
callback();
},
throttleWait,
options,
);
// throttle runAsync should be promise
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
fetchInstance.runAsync = (...args) => {
return new Promise((resolve, reject) => {
throttledRef.current?.(() => {
_originRunAsync(...args)
.then(resolve)
.catch(reject);
});
});
};
return () => {
fetchInstance.runAsync = _originRunAsync;
throttledRef.current?.cancel();
};
}
}, [throttleWait, throttleLeading, throttleTrailing]);
if (!throttleWait) {
return {};
}
return {
onCancel: () => {
throttledRef.current?.cancel();
},
};
};