持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第18天,点击查看活动详情
这期主要讲useRequest这个库的用法以及源码实现,感觉是和react-query类似的东西。当前是蚂蚁中台最佳实践内置网络请求方案。
一个强大的异步数据管理的hooks,react项目的网络请求用useRequest就够了
场景:蚂蚁中台最佳实践内置网络请求方案。存在于umi中。
特点和react-query
很像
特点:
- 自动请求
- 轮询
- 防抖节流
- 屏幕聚焦重新请求
- 错误重试
- loading、delay
- SWR、swr 是
stale-while-revalidate
的简称,最主要的能力是:我们在发起网络请求时,会优先返回之前缓存的数据,然后在背后发起新的网络请求,最终用新的请求结果重新触发组件渲染。swr 特性在特定场景,对用户非常友好。 - 缓存
知乎文档:zhuanlan.zhihu.com/p/106796295
官方文档:ahooks.gitee.io/zh-CN/hooks…
安装: npm i ahooks --save
依赖的hooks
们
useUpdateEffect
同useEffect
,但只会依赖更新时执行,不会初始化执行useCreation useMemo
或useRef
的替代品useLatest
返回最新值,避免闭包useMemoizedFn
持久化function
的hook
,理论上可以代替useCallback
useMount
只在组件初始化执行useUnmount
组件卸载执行useUpdate
返回一个函数,调用会强制组件重渲染
tips:建议熟悉useRef
等hook再来看
useLatest
返回最新值,避免闭包
import React from 'react'
// 创建ref指向value,避免闭包。多次渲染中保持不变
function useLatest(value) {
const ref = React.useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
useUpdate
返回一个函数,调用会强制组件重渲染
import React, { useCallback } from 'react';
//调用 setState 更新 进行 重渲染 ,使用useCallback 缓存函数避免每次调都新建一个函数
function useUpdate() {
const [, setState] = React.useState({});
return useCallback(() => setState({}), []);
}
export default useUpdate;
useCreation
文档:ahooks.gitee.io/zh-CN/hooks…
因为 useMemo
不能保证被 memo 的值一定不会被重计算,而 useCreation
可以保证这一点。
import React from 'react';
import depsAreSame from '../utils/depsAreSame';
function useCreation(factory, deps) {
// 这里比较疑惑 deps 这里不是用参数缩写赋值了吗,为什么下面还要比较,说是比较上一个的deps和传入的deps的比较
//useRef 返回一个可变的 ref 对象(每次渲染时都会返回一个相同的引用,引用地址不会发生变化),其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
// 最后知道 React.useRef 只在初始化创建一次,后面的调用都没执行了。
const { current } = React.useRef({
deps,
obj: undefined,
initialized: false//是否初始化过 我是觉得initialized不用也行。
});
// 是否初始化过,依赖数组是否变动。
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj;
}
export default useCreation;
// 浅比较 先比较两者是否强等于,再依次判断内容是否Object.is相等
function depsAreSame(oldDeps, newDeps) {
if (oldDeps === newDeps) return true;
for (let i = 0; i < oldDeps.length; i++) {
if (!Object.is(oldDeps[i], newDeps[i])) return false;
}
return true;
}
useMount
//这样就能在初始化只执行一次了
import React from 'react';
function useMount(fn) {
React.useEffect(() => {
fn();
}, []);
}
export default useMount;
useMemoizedFn
代替 useCallback
function useMemoizedFn(fn) {
const fnRef = useRef(fn);
// 当fn变了更新,不变时不更新
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef();
if (!memoizedFn.current) {
// 这里保证 引用不会变,这里永远是这个函数。
memoizedFn.current = function (...args) {
return fnRef.current.apply(this, args);
}
}
return memoizedFn.current;
}
useUnmount
组件卸载
import useLatest from '../useLatest';
import React, { useEffect } from 'react';
function useUnmount(fn) {
const fnRef = useLatest(fn);
useEffect(() => {
return () => {
fnRef.current()
}
}, []);
}
export default useUnmount;
实现小例子
function getName(suffix = '') {
//请求
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ data: '111', });
}, 2000);
});
}
const {data,loading} = useRequest(getName)
------
class Fetch{
constructor(serviceRef ,subscribe){
this.serviceRef = serviceRef
this.subscribe = subscribe // 重渲染
this.state = {loading:false,data:undefined}
}
setState(s){
this.state = {...this.state,...s}
this.subscribe()
}
//异步
async runAsync(){
this.setState({loading:true})
const data = await this.serviceRef.current() //请求
this.setState({loading:true,data})
}
//同步
run(){
//同步里放异步
this.runAsync()
}
}
function useRequest(service){
const serviceRef = useLatest(service) // 用useRef 封装请求,跨组件
const update = useUpdate() // 重渲染
// 返回单例的 请求实例 useCreation = useMemo 防止重渲染的时候重新生成
const fecthInstance = useCreation(()=>new Fetch(serviceRef,update),[])
// 只在初始化的时候跑一次请求 useEffect(()=>xx,[])
useMount(()=>{
fecthInstance.run()
})
return {
loading:fecthInstance.state.loading,
data:fecthInstance.state.data,
}
}
请求返回error
async runAsync(){
this.setState({loading:true})
const data = await this.serviceRef.current() //请求
this.setState({loading:true,data})
}
// 改造
=============>
async runAsync(){
this.setState({loading:true})
try{
const data = await this.serviceRef.current() //请求
this.setState({loading:true,data})
}catch(error){
this.setState({loading:true,data:undefined,error})
throw error
}
}
手动触发
const { data, loading, run,runAsync } = useRequest(getName, {
manual: true //表示手动触发请求,需要自己去调run,runAsync
});
---实现
function useRequest(service,options){
...
const {manual,...rest} = options
useMount(()=>{
if(!manual){
fecthInstance.run()
}
})
return {
loading:fecthInstance.state.loading,
data:fecthInstance.state.data,
run:fecthInstance.run.bind(fecthInstance),
// 可以优化成 useMemoizedFn(fecthInstance.run.bind(fecthInstance)),减少性能消耗
runAsync...
}
}
-- 异常处理 例子
const { data, loading, run,runAsync } = useRequest(getName, {
manual: true //表示手动触发请求,需要自己去调run,runAsync
onError:()=>{console.log('111')}
});
// 实现 同步会帮我们捕获 进行回调
run(){
this.runAsync().catch(e=>{
this.options.onError(e)
})
}
// 异步
runAsync().catch(e=>{
//我们需自己处理
})
传参
//例子
const { data, loading, run,runAsync } = useRequest(getName, {
defaultParams:['x']
});
run('a')
// 实现 例子里会自动触发一次,手动执行一次
// 自动 自动执行这里传参 useRequest.js
useMount(() => {
if (!manual) {
const params = fetchInstance.state.params || options.defaultParams || []
fetchInstance.run(...params);
}
});
// 手动 请求里传参数,params缓存
// fetch.js
run(...params) {
this.runAsync(...params).catch(error => {
...
});
}
async runAsync(...params){
const data = await this.serviceRef.current(...params) //请求
this.setState({ loading: false, data: res, error: undefined, params });
}
生命周期
useRequest
提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。
onBefore
:请求之前触发onSuccess
:请求成功触发onError
:请求失败触发onFinally
:请求完成触发
// 例子:
const { data, loading, run,runAsync } = useRequest(getName, {
onBefore(params){
console.log('请求前',params)
},
onFinally(params,res,error){
console.log('请求结束',params,res,error)
},
});
// 实现
async runAsync(...params) {
this.options.onBefore?.(params);
try {
const data = await this.serviceRef.current() //请求
this.options.onSuccess?.(res, params);
this.options.onFinally?.(res, params);
} catch (error) {
this.options.onError?.(error, params);
this.options.onFinally?.(error, params);
}
}
刷新(重复上一次请求)
useRequest
提供了 refresh
和 refreshAsync
方法,使我们可以使用上一次的参数,重新发起请求。
const { data, loading, refresh } = useRequest(getName)
----》 每次请求,缓存参数,使用refresh方法时调用run,参数是缓存的参数即可
//fetch.js
run(...params){
this.setState(params)
}
refresh(){
this.run(this.state.params)
}
//useRequest.js
return {
loading:fecthInstance.state.loading,
...
refresh:useMemoizedFn(fecthInstance.refresh.bind(fecthInstance)),减少性能消耗
}
乐观更新
useRequest
提供了 mutate
, 支持立即修改 useRequest
返回的 data
参数。
mutate
的用法与 React.setState
一致,支持 mutate(newData)
和 mutate((oldData) => newData)
两种写法。
const lastRef = useRef() //备份更新前的值,用来错误回滚
const {data:name,mutate} = useRequest(getName,{}) // 只请求了一次。
const { run } = useRequest(updateName,{ // 用来请求更新数据
manual:true,
onSuccess(result,params){
setValue('')
},
onError(e){
mutate(lastRef.current)
}
})
onclick={()=>{
lastRef.current = name
mutate(value); // 直接更新
run(value)} // 请求更新
}
return <div>{name}</div>
// 改值时
//fetch.js 没请求,只是返回的数据
mutate(data){
let target = data
this.setState({data:target})
}
//useRequest.js
return {
loading:fecthInstance.state.loading,
...
mutate:useMemoizedFn(fecthInstance.mutate.bind(fecthInstance)),减少性能消耗
}
取消请求
useRequest
提供了 cancel
函数,可以取消当前正在进行的请求。同时 useRequest
会在以下时机自动取消当前请求:
- 组件卸载时,取消正在进行的请求
- 竞态取消,当上一次请求还没返回时,又发起了下一次请求,则会取消上一次请求
const { cancel } = useRequest(updateName,{onCancel:回调})
// 实现
//fetch.js
this.count = 0
async runAsync(){
// 举个例子,如果连续两次请求,第一次里的count会不相等,返回取消
this.count++
const currentcount = this.count
const data = await this.serviceRef.current()
// 数据不相同,说明在期间调用了cancel
if(currentcount !== this.count){
return new Promise(()=>{})
//返回空,表示取消 ,虽然还是去请求了。没有真正的取消
}
}
cancel(){
this.count++
this.setState({loading:false})
this.options.onCancel()
}
//useRequest.js
//组件卸载时,取消正在进行的请求
useUnmount(()=>fecthInstance.cancel())
下次讲 插件系统