受到ahooks的useRequest启发,vue-hooks-plus借鉴了ahooks的思想,并且大部分代码是可以复用。
下面就以vue-hooks-plus的useRequest来分析实现原理
ts
const {
loading: Readonly<Ref<boolean>>,
data?: Readonly<Ref<TData>>,
error?: Readonly<Ref<Error>>,
params: Readonly<Ref<TParams | []>>,
run: (...params: TParams) => void,
runAsync: (...params: TParams) => Promise<TData>,
refresh: () => void,
refreshAsync: () => Promise<TData>,
mutate: (data?: TData | ((oldData?: TData) => (TData | undefined))) => void,
cancel: () => void,
} = useRequest<TData, TParams>(
service: (...args: TParams) => Promise<TData>,
{
manual?: boolean,
defaultParams?: TParams,
formatResult?:(response:TData)=>unknown,
onBefore?: (params: TParams) => void,
onSuccess?: (data: TData, params: TParams) => void,
onError?: (e: Error, params: TParams) => void,
onFinally?: (params: TParams, data?: TData, e?: Error) => void,
}
);
useRequest返回的这些方法,基本上都是挂载在Fetch实例上的,下面以Fetch为核心进行分析
核心 - Fetch
Fetch的代码如下
js
export default class Fetch<TData, TParams extends unknown[] = any> {
// 插件配置
pluginImpls: UseRequestPluginReturn<TData, TParams>[] | undefined;
count = 0;
// 内部请求状态
state: UseRequestFetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
// 执行相关方法
runAsync(){...};
run(){...};
refreshAsync(){...};
refresh(){...};
cancel(){...};
mutate(){...};
}
Fetch被封装成一个class,我觉得可以分成三块来看:插件作用,请求状态,执行相关方法;
1.插件作用
对于插件,我们不妨先这样理解,插件是一个use函数,返回一个插件作用:
一个useXxxPlugin(插件)会返回一个 pluginImpls(插件作用), 上面👆🏻pluginImpls大概结果如下
pluginImpls : [{onBefore, onRequest, onSuccess, onError, onFinally}]
onXx都是一个函数,onBefore和onRequest是有返回值要求的。 例如:
js
// onBefore的返回值
{
stopNow?: boolean;
returnNow?: boolean;
loading?: boolean;
params?: TParams;
data?: TData;
error?: Error | unknown;
}
然后定义了一个方法来执行插件作用数组的某一个"作用",这些作用其实是和请求的生命周期对应的,即在请求的生命周期,执行对应的"作用"
js
/**
* Traverse the plugin that needs to be run,
* 插件的回调函数, 用于执行插件的逻辑.
*/
runPluginHandler(event: keyof UseRequestPluginReturn<TData, TParams>, ...rest: unknown[]) {
// @ts-ignore
const r = (this.pluginImpls?.map(i => i[event]?.(...rest)) ?? [])?.filter(Boolean);
// @ts-ignore
return Object.assign({}, ...r);
}
runPluginHandler 就是将pluginImpls (插件作用数组)的某个作用返回值结果赋值到一个对象上(后面的作用结果会覆盖前面的)。最终结果长下面这样
js
// runPluginHandler 返回结果
{
stopNow?: boolean;
returnNow?: boolean;
loading?: boolean;
params?: TParams;
data?: TData;
error?: Error | unknown;
}
2.请求状态
首先是内部定义了请求的状态
js
state: UseRequestFetchState<TData, TParams> = {
loading: false, //加载张图
params: undefined, //请求参数
data: undefined, //请求结果
error: undefined, //错误
};
另外还设置了一个修改state的方法 setFetchState
,注意这个方法是需要调用外面的方法来修改外部的响应式变量。具体来说就是在useRequestImplement层:
js
// reactive
const state = reactive<UseRequestFetchState<TData, TParams>>({
data: initialData,
loading: false,
params: undefined,
error: undefined,
});
const setState = (currentState: unknown, field?: keyof typeof state) => {
if (field) {
state[field] = currentState as any;
} else {
if (isUseRequestFetchState<UnwrapRef<TData>, UnwrapRef<TParams>>(currentState)) {
state.data = currentState.data;
state.loading = currentState.loading;
state.error = currentState.error;
state.params = currentState.params;
}
}
};
const initState = plugins.map(p => p?.onInit?.(fetchOptions)).filter(Boolean);
// Fetch Instance
const fetchInstance = new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
setState,
Object.assign({}, ...initState, state),
);
在执行请求的生命周期,会使用setFetchState
方法去修改请求状态。那么就引出了第3部分------执行相关方法。
3.执行相关方法
重点只需要关注runAsync
方法,其他方法run/refreshAsync/refresh
都是基于runAsync的,所以主要逻辑实际在 runAsync
中
js
async runAsync(...params: TParams): Promise<TData> {
this.count += 1;
const currentCount = this.count;
const { stopNow = false, returnNow = false, ...state } = this.runPluginHandler(
'onBefore',
params,
);
// Do you want to stop the request
if (stopNow) {
return new Promise(() => { });
}
this.setState({
loading: true,
params,
...state,
});
// Do you want to return immediately
if (returnNow) {
return Promise.resolve(state.data);
}
// The 'onBefore' configuration item error no longer interrupts the entire code flow
try {
// Return before request
this.options.onBefore?.(params);
} catch (error) {
// The 'onBefore' configuration item error no longer interrupts the entire code flow
this.setState({
error,
loading: false,
});
this.options.onError?.(error as Error, params);
this.runPluginHandler('onError', error, params);
// Manually intercept the error and return a Promise with an empty status
return new Promise(() => { });
}
try {
// Start the request with the replace service, if it contains the onRequest event name
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.value, params);
const requestReturnResponse = (res: any) => {
// The request has been cancelled, and the count will be inconsistent with the currentCount
if (currentCount !== this.count) {
return new Promise(() => { });
}
// Format data
const formattedResult = this.options.formatResult ? this.options.formatResult(res) : res;
this.setState({
data: formattedResult,
error: undefined,
loading: false,
});
// Request successful
this.options.onSuccess?.(formattedResult, params);
this.runPluginHandler('onSuccess', formattedResult, params);
this.previousValidData = formattedResult;
// Execute whether the request is successful or unsuccessful
this.options.onFinally?.(params, formattedResult, undefined);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, formattedResult, undefined);
}
return formattedResult;
};
if (!servicePromise) {
servicePromise = this.serviceRef.value(...params);
}
const servicePromiseResult = await servicePromise;
return requestReturnResponse(servicePromiseResult);
} catch (error) {
if (currentCount !== this.count) {
return new Promise(() => { });
}
this.setState({
error,
loading: false,
});
this.options.onError?.(error as Error, params);
this.runPluginHandler('onError', error, params);
// rollback
if (
(isFunction(this.options?.rollbackOnError) && this.options?.rollbackOnError(params))
|| (isBoolean(this.options?.rollbackOnError) && this.options.rollbackOnError)
) {
this.setState({
data: this.previousValidData,
});
}
// Execute whether the request is successful or unsuccessful
this.options.onFinally?.(params, undefined, error as Error);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
throw error;
}
}
其实可以发现就是在 onBefore/onRequest/onSuccess/onError/onFinally
这几个生命周期先执行"插件作用"
值得注意的几个点 :
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.value, params);
说明onRequest
作用是可能调用 service 产生promise的,如果插件没有调用,那么就在Fetch中调用。然后用requestReturnResponse
来处理这个promise.this.count
和currentCount
的作用: runAsync多次,只保留最后一次的结果
这里有个疑惑,如果多个插件调用了service产生promise,岂不是有的promise 创建了但浪费了? ------ 确实会有这种可能,但源码中给出了解决方法,参考useCachePlugin.
使用cachePromise 来保证只有一个service promise!
js
onRequest: (service, args) => {
let servicePromise = cachePromise.getCachePromise(cacheKey)
// 如果存在servicePromise,并且它没有被触发,则使用它
if (servicePromise && servicePromise !== currentPromiseRef.value) {
return { servicePromise }
}
servicePromise = service(...args)
currentPromiseRef.value = servicePromise
cachePromise.setCachePromise(cacheKey, servicePromise)
return { servicePromise }
}
插件
1.首先看插件的使用,第三个参数传递 「插件数组」
js
const { data } = useRequest(
() => serviceFn(),
{
...option,
pluginOptions: {
...pluginOption,
},
},
[useFormatterPlugin, ...otherPlugins],
)
2.在useRequestImplement层:
plugins: [useFormatterPlugin, ...otherPlugins]
3.到Fetch层(执行useXxxPlugin后结果给到Fetch):
pluginImpls [{onBefore, onRequest, onSuccess, onError},{...}, ...]
插件是一个use函数,在useRequest里的流程,6个生命周期
- onInit
- onBefore
- onRequest
- onError
- onSuccess
- onFinally
- onMutate
下面将分析内置的插件Plugin
useCachePlugin - 缓存
预备知识
首先介绍源码中的三个重要的工具
utils/cache.ts
使用Map缓存数据,但是有时间限制(使用setTimeout清理超时的数据)
js
setCache(
key: CachedKey,
cacheTime: number, //缓存时间,单位毫秒
cachedData: CachedData
)
utis/cachePromise.ts
:
使用Map缓存Promise (当promise结束了删除对应缓存)
utils/cacheSubscribe.ts
:
订阅发布的实现。暴露3个API trigger, subscribe, otherSubscribe
插件的参数
该插件的options(使用useRequest传递option,其中定义了下面部分就会被当做useCachePlugin的options)定义体现在第二个参数上,如下:
js
useCachePlugin(
fetchInstance,
{
cacheKey,
cacheTime = 5 * 60 * 1000,
staleTime = 0,
setCache: customSetCache,
getCache: customGetCache,
},
)
- 如果定义了cacheKey,那么就会启动缓存
- cacheTime和staleTime的区别:cacheTime是对缓存而言用来缓存数据的,先使用缓存数据,等请求新数据到了做替换;staleTime是为了用来避免短时间内重复请求的;
- 如果定义了setCache和getCache,就使用自定义的缓存。(默认使用Map来缓存的)
缓存实现的思路
onBefore中获取缓存: const cacheData = _getCache(cacheKey, params)
当获取的缓存数据在保鲜期staleTime内,那么就阻止后续流程,结束,直接返回数据。
js
// 数据是新鲜就停止请求
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) {
return {
loading: false,
data: cacheData?.data,
returnNow: true,
}
} else {
// 数据不新鲜,则返回data,并且继续发送请求
return {
data: cacheData?.data,
}
}
onSuccess中缓存结果:_setCache(cacheKey, { data, params, time: new Date().getTime(), })
,其中time就是之后用来计算是否超过保鲜期使用的。