useRequest 探究(一)

733 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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 useMemouseRef的替代品
  • useLatest 返回最新值,避免闭包
  • useMemoizedFn 持久化functionhook,理论上可以代替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

源码:github1s.com/alibaba/hoo…

文档: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 提供了 refreshrefreshAsync 方法,使我们可以使用上一次的参数,重新发起请求。

ahooks.gitee.io/zh-CN/hooks…

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)),减少性能消耗
    }

取消请求

源码:github1s.com/alibaba/hoo…

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())

下次讲 插件系统