rerender会造成throttle的一些问题
初步遇到问题
import React, { useState } from "react";
import { throttle } from "lodash";
function App() {
const [count, setCount] = useState(0);
const onChange = throttle(arg => {
console.log("onChange", arg);
setCount((preCount) => preCount + 1);
}, 1000);
return (
<div>
{count} <br />
<button onClick={() => onChange(count)}>click</button>
</div>
);
}
直接使用的话,因为按钮的点击触发的 onChange 中,有 setCount 的存在,而 setCount 会导致组件的 rerender,结果就是 onChange 会被重复赋值--也就是说,每次点击按钮都会触发一个新的 onChange 函数。
结果就是 thtottle 看起来是无效的。
去掉 setCount 就不会有这个问题,但是项目中的业务逻辑显然不会不触发组件的 rerender。那自定义一个 useThrottle 钩子,来尝试解决这个问题。
这个钩子的设计看起来有两个方向:
- 保证 useThrottle 函数的返回值在每次 rerender 后不变;
- 保证即使 useThrottle 函数的返回值会变化,但是内部的定时器不变;
总的来说,需要一个能够在 rerender 中,持久保存的数据。
方向一:保证 useThrottle 函数的返回值在每次 rerender 后不变
其实这里涉及到 ahooks/useCreation 中所说的:
而相比于
useRef,你可以使用useCreation创建一些常量,这些常量和useRef创建出来的 ref 有很多使用场景上的相似,但对于复杂常量的创建,useRef却容易出现潜在的性能隐患。
并且举出了一个例子:
const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了
const b = useCreation(() => new Subject(), []) // 通过 factory 函数,可以避免性能隐患
这个钩子提出了一个创建常量的想法,即被创建的变量/函数在首次创建或者 deps 发生变化之前,不需要被再次创建,可以参考他们的源代码:
function useCreation(factory = () => {}, deps = []) {
const { current } = useRef({ deps, obj: undefined, initialized: false });
if (current.initialized === false || !depsAreSame(current.deps, deps)) {
current.deps = deps;
current.obj = factory();
current.initialized = true;
}
return current.obj;
}
function depsAreSame(oldDeps = [], deps = []) {
if (oldDeps === deps) return true;
for (const i in oldDeps) {
if (oldDeps[i] !== deps[i]) return false;
}
return true;
}
以这个为思路,可以实现一个简单的 useThrottle 钩子:
import React, { useState } from "react";
import { throttle } from "lodash";
import { useCreation } from 'ahooks';
function App() {
const [count, setCount] = useState(0);
const onChange = useThrottle(arg => {
console.log("onChange", arg);
setCount((preCount) => preCount + 1);
}, 1000);
return (
<div>
{count} <br />
<button onClick={() => onChange(count)}>click</button>
</div>
);
}
// 自定义 useThrottle 钩子
function useThrottle(fn = () => {}, time = 300) {
return useCreation(() => throttle(fn, time));
}
当然 ahooks 是有 useThrottleFn 钩子的,可以直接使用 ahooks/useThrottleFn.
方向二:保证即使 useThrottle 函数的返回值会变化,但是内部的定时器不变
用 useRef 来保存 timer:
function useThrottle(fn = () => {}, time = 300) {
const { current } = useRef({ timer: null });
return (...args) => {
if (current.timer) return;
fn.apply(null, args);
current.timer = setTimeout(() => {
clearTimeout(current.timer);
current.timer = null;
}, time);
};
}
这样也能实现 throttle 的效果。