原文:How to Use Debounce and Throttle in React and Abstract them into Hooks,作者:Divyanshu Maithani
Hooks(钩子)是对React绝妙的补充。在使用class
组件时需要被分配到不同生命周期的逻辑,因为钩子而简化了不少。
当然使用钩子需要具备 不一样的 思考方式,特别是对于初次使用者来说。
同时,我也推荐一个视频合集,希望对你学习这个知识点有帮助。
防抖和节流
已经有许多介绍如何编写防抖和节流的博文,这里我就不赘述了。简单起见,你可以参考Lodash的debounce
和throttle
。
我们快速回顾一下,两种函数都接受一个(回调)函数, 一个以毫秒为单位的 延迟 (如 x
)并且返回另一个具有特殊行为的函数:
防抖
:返回一个可以被多次调用的函数(可能是快速连续调用),但仅在最后一次调用之后再等待x
毫秒后才触发回调。节流
:返回一个可以被多次调用的函数(可能是快速连续调用),但仅在每x
毫秒,触发一次回调。
用例
有一个迷你博客编辑器项目(这里是该项目的GitHub仓库),在这个编辑器中,我们希望用户停止输出后1秒钟将博文添加到数据库中。
你可以通过查看Codesandbox获取最终代码效果。
编辑器简化代码如下:
import React, { useState } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // 真实情况下这里应该调用API
const handleChange = event => {
setValue(event.target.value);
};
return (
<main>
<h1>Blog</h1>
<textarea value={value} onChange={handleChange} rows={5} cols={50} />
<section className="panels">
<div>
<h2>Editor (Client)</h2>
{value}
</div>
<div>
<h2>Saved (DB)</h2>
{dbValue}
</div>
</section>
</main>
);
}
在这段代码中saveToDb
在真实场景应该用于向后端发起API调用。在这里我们做简化处理,将数据存入状态(state)然后作为 dbValue
渲染。
因为我们仅想在用户停止输入后(1秒后),执行存储行为,所以我们使用的是 防抖。
这里是起始代码库和分支。
创建一个防抖函数
首先,我们需要一个防抖函数封装对saveToDb
的调用:
import React, { useState } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // 真实情况下这里应该调用API
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// 防抖函数开始
const debouncedSave = debounce(() => saveToDb(nextValue), 1000);
debouncedSave();
// 防抖函数结束
};
return <main>{/* 和之前代码一致 */}</main>;
}
但这段代码并不生效,因为 debouncedSave
在每次 handleChange
调用都会被重新创建。这样会对每一次按键的输入的值起作用,而不是整个输入值。
useCallback
当给子组件传入调用时,useCallback
可优化性能。我们可以借助它对回调函数的记忆化,来确保每次渲染debouncedSave
都指向同一个防抖函数。
我在freeCodeCamp也写过这篇文章,帮助你理解记忆化的基本知识。
这样代码就奏效了:
import React, { useState, useCallback } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // 真实情况下这里应该调用API
// 防抖函数开始
const debouncedSave = useCallback(
debounce(nextValue => saveToDb(nextValue), 1000),
[], // 仅在初次渲染调用
);
// 防抖函数结束
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// 即便每次渲染handleChange都会被创建和执行
// 每次都指向初次创建的debouncedSave
debouncedSave(nextValue);
};
return <main>{/* 和之前代码一致 */}</main>;
}
useRef
useRef
提供一个可以修改的对象,对象的current
属性指向传入的最初值。如果不手动修改,该值会在组件的整个生命周期保持不变。
这和类组件的实例属性类似(即使用this
定义的属性和方法)。
这样做也奏效:
import React, { useState, useRef } from 'react';
import debounce from 'lodash.debounce';
function App() {
const [value, setValue] = useState('');
const [dbValue, saveToDb] = useState(''); // 真实情况下这里应该调用API
// 在渲染中保持不变
// 防抖函数开始
const debouncedSave = useRef(debounce(nextValue => saveToDb(nextValue), 1000))
.current;
// 防抖函数结束
const handleChange = event => {
const { value: nextValue } = event.target;
setValue(nextValue);
// 即便每次渲染handleChange都会被创建和执行
// 每次都指向初次创建的debouncedSave
debouncedSave(nextValue);
};
return <main>{/*和之前一样*/}</main>;
}
你可以浏览我的博客 了解如何自定义钩子来抽象化这些概念,或者点击这个视频系列,了解更多内容。
你可以关注我的 Twitter 来获取最新信息,希望这篇文章对你有帮助。:)