一、前言
最近在重温js的相关知识,写了一个小demo, 想实现一下防抖和节流,情景如下:
页面效果:
点击+1按钮实现counter + 1效果,也就是counter会从原本的0变成1,再点击, 1变成2, 以此类推;
最初的demo代码
import React, {useState} from 'react';
export default function App1() {
const [counter, setCounter] = useState(0);
// +1 操作
const add = () => {
setCounter(counter + 1);
}
// 重置操作
const onReset = () => {
setCounter(0);
}
return (
<div>
<h1>counter: {counter}</h1>
<button onClick={add}>+1</button>
</div>
)
}
二、增加节流效果
既然想练习防抖和节流,那肯定是需要想让他的+1效果实现防抖和节流效果啦;
我们先使用lodash里面的throttle来实现节流,防抖(debounce)的使用和throttle的类似(我说的是使用,不是使用之后的最终效果哦),所以这里只写讲throttle;现在想给他增加的节流效果是:1秒内无论点击多少次,都只执行最后一次,代码如下:
import React, {useState} from 'react';
+ import _ from 'lodash';
export default function App1() {
const [counter, setCounter] = useState(0);
const add = () => {
setCounter(c => c + 1);
}
const onReset = () => {
setCounter(0);
}
+ const throttle1 = _.throttle(add, 1e3);
return (
<div>
<h1>counter: {counter}</h1>
<button onClick={add}>+1</button>
<button onClick={onReset}>重置</button>
+ <hr/>
+ <button onClick={throttle1}>节流1</button>
</div>
)
}
但是实现了吗?并没有,和没加防抖是一样的效果 为什么呢?首先我们要知道throttle的实现原理,就是使用时间戳或者定时器来实现的节流。react重新渲染页面的标准是:状态更改。由于counter被更改了,所以App1这个组件重新渲染了,也就是重新执行了一遍,重新执行的话,之前throttle内部的时间戳或定时器被重新赋值,throttle也就失去了作用。
App1组件重新渲染不可以避免,但是我们可以避免throttle内部的时间戳或定时器被重新赋值,所以要怎么做呢?对throttle函数进行一个缓存,就可以保留之前throttle函数内部的状态,实现节流;就要用到useCallback来的对throttle函数进行缓存
useCallback(callback, deps): callback是回调函数, deps是依赖项,以数组形式存在,当deps中的依赖项变化的话,useCallback会被重新执行;
代码变成如下:
import React, {useState, useCallback, useEffect} from 'react';
import _ from 'lodash';
export default function App1() {
const [counter, setCounter] = useState(0);
const add = () => {
setCounter(counter + 1 );
}
const onReset = () => {
setCounter(0);
}
const invalidThrottle1 = _.throttle(add, 1e3);
+ const throttle2 = useCallback(_.throttle(add, 1e3), []);
+ useEffect(() => {
console.log('重新渲染', counter);
})
return (
<div>
<h1>counter: {counter}</h1>
<button onClick={add}>+1</button>
<button onClick={onReset}>重置</button>
<hr/>
<button onClick={invalidThrottle1}>无效节流1</button>
+ <button onClick={throttle2}>节流2</button>
</div>
)
}
效果如下:
counter变成1之后就不再变化,无论怎么点击都不变化,所以节流实现了吗?
事实上,节流实现了,为啥?我们来证明一下,给add函数增加一个打印,发现控制台的输出是节流输出的(如下图),但是为啥没有实现我们想要的效果:counter也节流增加呢?
const add = () => {
setCounter(counter + 1 ); // 节流了 但是没有得到最新值,页面一直是1
console.log('throttle2', Date()); // 节流打印
}
其实是因为这里形成了一个由counte变量和add函数组合而成的闭包,而这个闭包被useCallBack缓存下来,所以add函数里面的counter一直都是最开始的0, add执行以后就一直是1,所以页面不会有变化, 而要打破这个闭包,就需要获取到最新的counter,如何获取?
两种方式:
- 使用setState的函数形式 将add函数变成以下:
const add = () => {
setCounter(c => c + 1);
}
- 配合ref使用
const latestCounter = useRef(0);
const add = () => {
setCounter(latestCounter.current + 1)
}
两种方式都可以得到以下的效果,也就是我们最先想要的效果
可能有的同学还会这样来使用useCallback
const invalidThrottle3 = useCallback(_.throttle(() => {
add()
}, 1e3), [counter]);
因为依赖counter,add重新执行,也就获取到了最新的counter,但是这样useCallback来缓存函数的意义就没有了,节流防抖也就失效了
总结
这里的坑主要是闭包导致的状态不能更新,打破闭包的方式,就是获取最新的counter: 两种方式:
- 使用setState的函数形式
- 配合ref使用
- 注意useCallback依赖项的使用