React Hooks 自 16.8 版本发布以来,彻底改变了函数组件的编写方式。他们不仅简化了状态管理和副作用处理,还极大地提升了代码地复用性和可维护性。也让类组件使用的越来越少了。
什么是React Hooks
React Hooks 是 React 16.8 版本引入的一组新特性,允许我们开发的时候在函数组件中使用状态和其他 React 特性,像之前的函数组件,是没有自己的状态的,并且没有生命周期一类的东西。 Hooks 的设计目的就是解决函数组件中状态管理和副作用处理的难题,同时增强代码的复用性和可读性。
为什么需要 Hooks
在 Hooks 之前,函数组件主要用于展示 UI,无法管理状态和处理副作用。这些功能只能通过类组件实现。然后,类组件存在一些缺点,如 this 绑定复杂,曾几何时,还有人记得 React 之前的绑定事件是这样的
import React, { Component } from 'react';
class MyButton extends Component {
constructor(props) {
super(props);
// 绑定 this
this.handleClick = this.handleClick.bind(this)
}
/**
* 类组件普通绑定事件
* 优点:性能较好,因为绑定操作只在构造函数中执行一次
* 缺点:需要再构造函数中进行额外的绑定操作,代码略显繁琐
*/
handleClick() {
console.log('按钮被点击了!');
}
/**
* 使用箭头函数作为类属性
* 优点:语法简洁,避免了构造函数中进行绑定,更易读和维护
* 缺点:需要配置 Babel 或其他编译器才能支持(大多数现代项目默认支持)
*/
handleClickArrow = () => {
console.log('按钮被点击了!');
}
/**
* 在 render 方法中使用箭头函数
* 优点:简单直观,无需在构造函数中绑定
* 缺点:每次渲染时都会创建一个新的函数,可能会影响性能,特别时在需要传递参数或在大量组件中使用时
*/
handleClickRender() {
console.log('按钮被点击了!')
}
render() {
return (
<>
<button onClick={this.handleClick}>
点我
</button>
<button onClick={this.handleClickArrow}>
点我箭头
</button>
<button onClick={() => this.handleClickRender()}>
点我render 中箭头
</button>
</>
)
}
}
Hooks 的优势
- 简化组件逻辑:函数组件可以取代类组件,减少样板代码,使用 Hooks 可以在函数组件中轻松管理状态和副作用
- 提升代码复用性:通过自定义 Hooks,可以将组件逻辑提取到可服用的函数中,避免重复代码。
- 增强可读性和可维护性:函数组件比类组件简洁, Hooks 进一步优化了代码结构
如何使用 React Hooks
React 提供了一组内置的 Hooks,涵盖了大部分常见的功能需求,使用也很简单,下面就举例最常用的
import { useState, useEffect, useRef } from 'react';
function Counter() {
const [count, setCount] = useState(0); // 初始化 count 为 0
const inputRef = useRef(null)
useEffect(() => {
if(inputRef.current) {
inputRef.current.focus()
}
}, [])
return (
<>
<input ref={inputRef} value={count} type="number" onChange={(e) => { setCount(e.target.value) }}/>
</>
)
}
使用很简单,具体可参看官网说明,简单易懂
那什么是自定义 Hooks 呢
自定义 Hooks 是以 use 开头的 Javascript 函数,它们可以调用其他的 Hooks,并封装特定的逻辑,以便在多个组件中服用。通过自定义 Hooks,咱们可以将组件逻辑抽象成可服用的函数,使代码更加模块化和易于维护。
为什么使用自定义Hooks
- 复用逻辑:在多个组件中共享相同的逻辑,而不需要重复编写相同的代码(其实类组件的时候我感觉也可以复用逻辑🥲)
- 抽象复杂性:将复杂的逻辑抽象到自定义 Hooks 中,使组件代码更简洁(理论上函数组件中只存在 hook 就行)
- 提高可读性:通过命名自定义 Hooks,可以更清晰地表达逻辑意图。
- 易于测试:自定义 Hooks 可以单独测试,提高代码质量。
如何创建自定义 Hooks
创建自定义 Hooks 的步骤如:
- 以
use开头命名:自定义 Hooks 必须以use开头,以遵循 React Hooks 的命名规范,并确保我们的 Hooks 能被 React 识别 - 调用内置 Hooks:在自定义 Hooks 中,可以调用其他内置的 Hooks,如
useState、useEffect等。 - 返回需要的值:自定义 Hooks 可以返回任何类型的数据,如状态、函数、对象等,供组件使用
编写自定义 Hook:防抖&节流
自定义 Hooks 允许我们将可复用的逻辑提取到独立的函数中,提升代码的复用性和可维护行。防抖(Debounce)和节流(Throttle)是常见的性能优化技术,适用于处理频繁触发的事件,有输入框的实时搜索,窗口大小的调整等。下面我们将编写两个自定义 Hook:useDebounce 和 useThrottle
防抖(Debounce)Hook
我们实现的防抖功能参考lodash/debounce,是返回一个防抖的函数,想在哪里使用都行
import { useState } from 'react';
/**
* 简单的防抖函数
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖时间
* @returns {Function} - 防抖后的函数
*/
function useDebounce(callback, delay) {
const [timer, setTimer] = useState(null);
const debouncedFunction = (...args) => {
// 如果已经有一个定时器在运行,清除它
if (timer) {
clearTimeout(timer);
}
// 创建一个新的定时器
const newTimer = setTimeout(() => {
callback(...args);
}, delay);
setTimer(newTimer);
}
return debouncedFunction;
}
export default useDebounce;
这应该是最基本的实现了,下面我们来看看如何优化
使用 useRef 确保组件重新渲染函数的稳定,没有清理并伴有内存泄露风险
-
由于我们使用
useState来存储定时器 ID,每次调用debouncedFunction时,setTimer会触发组件重新渲染,这不仅影响性能,还可能导致防抖功能失效 -
我们还需要在组件卸载时清除定时器,确保不会在卸载的组件上执行回调函数
import { useRef, useEffect, useState } from 'react';
/**
* 简单的防抖函数
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖时间
* @returns {Function} - 防抖后的函数
*/
function useDebounce(callback, delay) {
const timer = useRef(null);
// 返回一个防抖后的函数
const debouncedFunction = (...args) => {
// 清除上一次的定时器
if (timer.current) {
clearTimeout(timer.current);
}
// 创建一个新的定时器
timer.current = setTimeout(() => {
callback(...args);
}, delay);
}
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (timer.current) {
clearTimeout(timer.current);
}
}
}, []);
return debouncedFunction;
}
export default useDebounce;
使用 useCallback 确保返回的防抖函数在依赖项不变的情况下保持稳定,避免不必要的函数重新创建
使用useCallback来记忆debouncedFunction,使其在 callback 或 delay 不变时保持同一引用
import { useRef, useEffect, useCallback } from 'react';
/**
* 简单的防抖函数
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖时间
* @returns {Function} - 防抖后的函数
*/
function useDebounce(callback, delay) {
const timer = useRef(null);
// 返回一个防抖后的函数, 该函数会在 delay 时间后执行 callback
const debouncedFunction = useCallback((...args) => {
// 清除上一次的定时器
if (timer.current) {
clearTimeout(timer.current);
}
// 创建一个新的定时器
timer.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (timer.current) {
clearTimeout(timer.current);
}
}
}, []);
return debouncedFunction;
}
export default useDebounce;
使用 useRef 保存最新的函数
为了确保防抖函数在多次快速调用时正确工作,并且依赖项(如 callback)的变化能够被正确处理,也防止有 stale closure 问题(陈旧闭包问题,是指在 JavaScript 中,尤其是在 React 中,闭包内部捕获了某个变量的旧值,导致无法正确访问最新的值)
import { useRef, useEffect, useCallback } from 'react';
/**
* 简单的防抖函数
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖时间
* @returns {Function} - 防抖后的函数
*/
function useDebounce(callback, delay) {
const timer = useRef(null);
const callbackRef = useRef(callback);
// 返回一个防抖后的函数, 该函数会在 delay 时间后执行 callback
const debouncedFunction = useCallback((...args) => {
// 清除上一次的定时器
if (timer.current) {
clearTimeout(timer.current);
}
// 创建一个新的定时器
timer.current = setTimeout(() => {
callbackRef.current(...args);
timer.current = null;
}, delay);
}, [delay]);
// 每次渲染时更新 callback
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (timer.current) {
clearTimeout(timer.current);
}
}
}, []);
return debouncedFunction;
}
export default useDebounce;
现在已经近乎完美了,但是在真正的开发或者使用当中,我们可能还需要额外的功能,必须需要立即执行一次,而且可能会需要手动清除
自认为完美的 防抖(Debounce)Hook
结合 leading 参数和 cancel 方法,以下是最终优化后的 useDebounce Hook:
import { useRef, useEffect, useCallback } from 'react';
/**
* 完美的防抖函数
* @param {Function} callback - 需要防抖的函数
* @param {number} delay - 防抖延迟时间(毫秒)
* @param {Object} options - 可选参数
* @param {boolean} options.leading - 是否在防抖周期开始时立即调用
* @returns {Object} - 返回防抖函数及取消方法
*/
function useDebounce(callback, delay, options = {}) {
const { leading = false } = options; // leading 参数决定是否在延迟开始时立即调用
const timer = useRef(null); // 保存定时器 ID
const callbackRef = useRef(callback); // 保存回调函数的引用
// 防抖函数
const debouncedFunction = useCallback((...args) => {
if (leading && !timer.current) {
callbackRef.current(...args); // 如果 leading 为 true 且没有定时器,则立即调用回调函数
}
if (timer.current) {
clearTimeout(timer.current); // 清除之前的定时器
}
timer.current = setTimeout(() => {
callbackRef.current(...args); // 延迟结束后调用回调函数
timer.current = null; // 重置定时器
}, delay)
}, [delay, leading]);
// 取消防抖
const cancel = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current); // 清除定时器
timer.current = null; // 重置定时器
}
}, [])
// 更新回调函数引用
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
cancel()
}
}, [cancel]);
return { debouncedFunction, cancel}; // 返回防抖函数和取消函数
}
export default useDebounce
节流(Throttle)Hook
同理,相同的逻辑不多说,直接上最后的自认为完美的实现
import { useRef, useEffect, useCallback } from 'react';
/**
* 完美的节流
* @param {Function} callback - 需要节流的函数
* @param {number} delay - 节流延迟时间(毫秒)
* @param {Object} options - 可选参数
* @param {boolean} options.leading - 是否在节流周期开始时立即调用
* @param {boolean} options.trailing - 是否在节流周期结束时调用
* @returns {Object} - 返回节流函数及取消方法
*/
function useThrottle(callback, delay, options = {}) {
const { leading = false, trailing = false } = options; // leading 和 trailing 参数决定调用时机
const timer = useRef(null); // 保存定时器 ID
const lastArgs = useRef(null); // 保存最后一次调用的参数
const canRun = useRef(leading); // 标记是否可以运行
const callbackRef = useRef(callback); // 保存回调函数的引用
// 更新回调函数引用
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// 节流函数
const throttledFunction = useCallback((...args) => {
if (canRun.current) {
callbackRef.current(...args);
canRun.current = false;
timer.current = setTimeout(() => {
canRun.current = true;
if (trailing && lastArgs.current) {
callbackRef.current(...lastArgs.current);
lastArgs.current = null;
}
}, delay);
} else if (trailing) {
lastArgs.current = args;
}
}, [delay, trailing]);
// 取消节流
const cancel = useCallback(() => {
if (timer.current) {
clearTimeout(timer.current); // 清除定时器
timer.current = null; // 重置定时器
}
canRun.current = leading; // 根据 leading 重置
lastArgs.current = null;
}, [leading]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
cancel();
};
}, [cancel]);
return { throttledFunction, cancel }; // 返回节流函数和取消函数
}
export default useThrottle;
自定义 Hook 想明白了还是可以写的很快很方便的,使用起来可以节省很多时间,让代码看起来很整洁,逻辑很清晰
如果代码中有什么问题欢迎指正