likes
comments
collection
share

hook实现防抖

作者站长头像
站长
· 阅读数 19

背景

最近在学react,觉得hook来实现防抖的思路非常巧妙,故记录。定位已经掌握基本hook使用的读者,如果不会hook的话,可以先重点学下函数组件、useState和useEffect两个hook再来看此文。

防抖是什么

防抖即一种控制请求数量的技术。用户输入内容后我们发起延迟网络请求「比如延迟1min」,而不是直接发送,在间隔请求期间如果有新内容输入,就创建一个新的延迟网络请求,去覆掉上一次的请求内容。

为什么需要防抖

例如网站的搜索功能,根据关键字进行搜索,如果不进行防抖设置,那么输入'hello',就会发起5次网络请求,如果进行了防抖,那只要我们在间隔时间内输入内容,就会覆盖掉之前请求,并且以新的请求为准。

hook知识

📌创建useState

useState是react创建变量的hook,下面使用useState创建了一个sate,返回一个数组「下标0为变量名,下标1为操作变量的方法」,我们解构到msg和setMsg两个变量中。

const [msg, setMsg] = useState('')

📌使用input事件和setMsg

我们在页面中使用onInput来监听输入事件,并在回调中调用setMsg去改变msg的值,input就会更新

...
const [msg, setMsg] = useState('')
...
+ return <div>
+     <input type="text" onInput={(e)=>setMsg(e.target.value)} />
+     <p>你输入了{input}</p>
+ </div>

📌创建useEffect

useEffect是另一个hook,结构为useEffect(()=>{...}, [dep1, dep2, ..., depN])当dep中的数据变化,就会执行参数1的回调函数。我们称参数1的回调函数为一个effect函数。 下面我们创建一个监听msg的useEffect。

...
const [msg, setMsg] = useState('')
+ useEffect(()=>{
+     console.log(msg)
+ }, [msg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

当我们在页面中往input输入内容后的流程为:用户输入内容=》setMsg调用=》useEffect监听到msg变化=》effect函数执行,即会打印出最新的msg

📌useEffect返回值

effect函数支持返回一个回调函数,用于做一些清理工作,比如清理本次添加的定时器,非必须,但有些场景非常有用。 比如说我们基于前面代码添加一个功能,用户输入内容后setTimeout任务,在1s之后进行弹框输入的内容。就可以下面这样写:

...
const [msg, setMsg] = useState('')
useEffect(()=>{
    console.log(msg)
+     const id =setTimeout(()=>alert(msg), 1000)
}, [msg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

这段代码有个很明显的问题,即用户输入5个字符就会触发5次setMsg,就会触发5次effect函数执行,就会 添加五个setTimeout,就会alert5次,不太优雅,我们可以利用effect改造为如果有新输入就取消上一次的定时器。下面我们可以用effect返回的回调函数来实现这个需求。 代码如下:

...
const [msg, setMsg] = useState('')
useEffect(()=>{
    console.log(msg)
    const id =setTimeout(()=>alert(msg), 1000)
    const id = setTimeout()
+     return ()=>clearTimeout(id)
}, [msg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

hook实现防抖

其实hook实现防抖的技术在上面的hook知识点已经都给出了,我们就是要用组合技术的方式来实现这个最终需求。

预期

输入内容,用setTimeout延迟进行网络请求,如果在延迟期间有新内容进来,就取消上一个定时器,开启新的延迟请求。

实现

1、创建一个msg来收集用户输入

...
const [msg, setMsg] = useState('')
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

过程:用户输入=》setMsg调用=》msg改变

2、创建一个effect来监听msg的改变并执行effect函数

...
const [msg, setMsg] = useState('')
+ useEffect(()=>{
+     console.log(msg)
+ }, [msg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

过程:用户输入=》setMsg调用=》msg改变=》执行effect函数中的输出msg。即用户输入后最终会执行effect。

我们最终希望输入后发起延迟网络请求,然后effect会在输入后执行,所以可以把网络请求处理逻辑写在effect里面。

3、在effect中添加网络防抖逻辑以及清理逻辑

在effect中添加定时器,延迟1s后执行,在定时器回调中进行网络请求,并在effect返回值中清理定时器。

...
const [msg, setMsg] = useState('')
useEffect(()=>{
    console.log(msg)
+     const id = setTimeout(()=>{利用用户输入内容msg进行网络请求...}, 1000)
+     return () => clearTimeout(id)
}, [msg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

过程:用户输入'h'=》setMsg调用=》msg改变为'h'=》页面重新渲染=》执行effect函数=》创建定时器1s后发起网络请求'h'=》返回清理函数=》1s之内输入了'e'=》setMsg调用=》msg改变为'he'=》执行上次effect中的返回的清理函数清理掉旧定时器=》执行本次effect函数=》创建定时器1s后发起网络请求'he'=》用户不再输入新内容=》1s时间过后发起he的网络请求。

🤔我们现在的代码有哪些问题?没错,无法复用,因为网络请求写死在防抖中了,为了实现复用,我们要将网络请求挪到外面,将防抖逻辑留在原地,那突破口在哪里呢?在于setTimeout中我们到底要干嘛,目前我们是进行了网络请求,如果要将网络请求写在外面,就要保证setTimeout能够触发网络请求的执行。思考一下,state和useEffect的联动,是不是setState会触发监听了此state的effect执行?那我们是不是可以将网络请求写在effect中,并让effect监听一个新的state,然后在setTimeout中执行setState,这样子就从定时器到期=》进行网络请求变成了定时器到期=》触发setState=》触发effect =》执行网络请求,思路有了,代码如下。

...
const [msg, setMsg] = useState('')
+ const [postMsg, setPostMsg] = useStat(msg)
useEffect(()=>{
    console.log(msg)
-    const id = setTimeout(()=>{利用用户输入内容msg进行网络请求...}, 1000)
+    const id = setTimeout(()=>{setPostMsg(msg)}, 1000)
    return () => clearTimeout(id)
}, [msg])
+ useEffect(()=>{
+     执行postMsg网络请求
+ }, [postMsg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

🤔虽然已经拆出来了,但还没有分文件,我们可以将防抖封装称为一个自定义hook,如下:

// util.ts
export const useDebounce(msg) {
    const [postMsg, setPostMsg] = useStat(msg)
    useEffect(()=>setPostMsg(msg), [msg])
    return postMsg
}

// 组件.tsx
import {useDebounce} from '../util'
...
const [msg, setMsg] = useState('')
const postMsg = useDebounce(msg)
useEffect(()=>{
    执行postMsg网络请求
}, [postMsg])
...
return <div>
    <input type="text" onInput={(e)=>setMsg(e.target.value)} />
    <p>你输入了{input}</p>
</div>

至此我们实现了hook防抖的全部功能。