原文:How to Use Debounce and Throttle in React and Abstract them into Hooks,作者:Divyanshu Maithani

Hooks(钩子)是对React绝妙的补充。在使用class组件时需要被分配到不同生命周期的逻辑,因为钩子而简化了不少。

当然使用钩子需要具备 不一样的 思考方式,特别是对于初次使用者来说

同时,我也推荐一个视频合集,希望对你学习这个知识点有帮助。

防抖和节流

已经有许多介绍如何编写防抖和节流的博文,这里我就不赘述了。简单起见,你可以参考Lodash的debouncethrottle

我们快速回顾一下,两种函数都接受一个(回调)函数, 一个以毫秒为单位的 延迟 (如 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 来获取最新信息,希望这篇文章对你有帮助。:)