在现代前端开发中,React 无疑是最具影响力的框架之一。而随着 React 的不断发展,Hooks(钩子) 的引入彻底改变了我们编写组件的方式。它简化了状态管理和副作用处理,让函数组件具备了类组件所拥有的强大功能,同时避免了复杂的继承结构和繁琐的高阶组件。
如果你曾经为类组件中的 this
指向问题感到困扰,或者面对复杂的状态逻辑时觉得难以维护,那么 React Hooks 就是你的救星。它提升了代码的可读性与可测试性。
本文将带你深入理解 React 中最常用的六个 Hook ------ useState
、useEffect
、useLayoutEffect
、useReducer
、useRef
和 useContext
,帮助你掌握这些现代 Web 开发的"神兵利器"。
1. useState - 状态管理
概念介绍: useState
定义一个响应式变量,提供专门的方法修改该变量的值
基本语法:
js
const [state, setState] = useState(initialState);
state
: 当前状态值。setState
: 更新状态的方法,接受新状态作为参数或返回新状态的函数(推荐用于基于之前状态的更新)。initialState
: 状态的初始值。
案例分析:计数器
js
function Counter() {
const [count, setCount] = useState(0); // 初始化计数为0
return (
<div>
<p>当前计数: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>减少</button>
</div>
);
}
在这个例子中,每当用户点击按钮时,我们通过调用
setCount
方法更新 count
的值,这会导致组件重新渲染并显示最新的计数值。
2. useEffect - 副作用处理
概念介绍: useEffect
Hook 可以让执行副作用操作,如数据获取、设置订阅以及手动更改 DOM。默认情况下,它会在每次渲染后运行,但可以通过提供依赖项数组来控制它的触发条件。
基本语法:
- 首次渲染 :无论依赖项是否有值,
useEffect
都会在组件首次挂载时执行一次。
scss
useEffect(() => {
// effect (副作用代码)
return () => {
// cleanup (清理代码)
};
}, [dependencies]);
三种主要使用方式
1. 无依赖数组 - 每次渲染后都执行
js
useEffect(() => {
console.log('组件每次渲染后都会执行');
});
在 React 的 useEffect
Hook 中,如果不传第二个参数(即不提供依赖数组) ,那么这个副作用函数将在组件每次完整渲染之后都会执行一次。这意味着它不仅会在首次挂载时运行,也会在每次状态更新、props 变化等引起的重新渲染后运行。
2. 空依赖数组 - 仅在挂载时执行
js
useEffect(() => {
console.log('仅在组件挂载时执行一次');
return () => {
console.log('仅在组件卸载时执行一次');
};
}, []);
在 React 的 useEffect
Hook 中,当我们传入一个空数组 作为依赖项(即 []
),这个副作用函数将只在组件首次挂载时执行一次,并在组件卸载时运行清理函数。这种模式非常适合用于初始化和销毁阶段的操作。
3. 有依赖项 - 依赖变化时执行
js
useEffect(() => {
console.log('当count变化时执行');
return () => {
console.log('在下一次effect执行前或组件卸载时清理');
};
}, [count]);
- 如果依赖数组中的值(如
count
)发生了变化,React 会先调用上一次 effect 的清理函数(如果有的话),然后执行新的 effect。 - 如果依赖项没有变化,则不会重新执行这个
useEffect
。
3. useLayoutEffect - 同步副作用
在 React 中,useLayoutEffect
是 useEffect
的"近亲",但它有一个关键区别:它会在 DOM 更新之后、浏览器绘制之前同步执行。这意味着你可以在这个阶段安全地读取 DOM 布局信息(如宽高、位置等),并在绘制前进行调整。
js
useLayoutEffect(() => {
// 同步操作 DOM 或测量布局
return () => {
// 清理逻辑(可选)
};
}, [dependencies]);
为什么 useLayoutEffect 可能导致掉帧?
因为 useLayoutEffect
是同步执行的,且在浏览器绘制之前运行。如果其中的代码执行时间过长,会延迟浏览器的绘制过程,导致用户感知到界面卡顿或"掉帧"。
特性 | useEffect | useLayoutEffect |
---|---|---|
执行时机 | 在浏览器绘制之后异步执行 | 在 DOM 更新后、浏览器绘制之前同步执行 |
对渲染的影响 | 不会阻塞浏览器渲染 | 会阻塞浏览器渲染 |
使用场景 | 大多数副作用场景 | 需要同步读取/操作 DOM 的场景 |
性能影响 | 较少影响性能 | 可能导致掉帧(如果逻辑复杂) |
4. useReducer - 复杂状态逻辑
概念介绍: 在 React 开发中,当我们面对复杂的状态结构或多个子值之间存在多个互相关联的状态逻辑时,使用 useState
可能会导致组件内部状态管理混乱、难以维护。这时,useReducer
就成为了一个更优的选择。 基本语法:
js
const [state, dispatch] = useReducer(reducer, initialState);
案例分析:计数器应用
js
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error();
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
</div>
);
};
export default Counter;
在这个例子中,我们使用
useReducer
实现了一个简单的计数器功能。点击按钮后,通过 dispatch
发送 increment
或 decrement
动作,reducer 根据动作类型更新状态。
5. useRef - 获取DOM引用及可变值
概念介绍: useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。它可以用来访问 DOM 元素或保持任何可变值,而不会导致组件重新渲染。
js
const ref = useRef(initialValue);
-
ref.current
是一个可变属性,可以存储任意值(DOM 节点、数值、对象等)。 -
ref.current
的变化不会引起组件重新渲染。 -
通常用于:
- 访问 DOM 元素
- 保存不需要参与渲染的状态数据
- 在回调函数中捕获最新的状态或 props
案例分析:聚焦输入框
js
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}
在这个示例中,useRef
被用来创建一个引用,该引用指向输入框元素,使得可以通过编程方式聚焦到该输入框。
6. useContext - 跨组件通信
概念介绍: useContext
Hook 接收一个上下文对象并返回当前上下文值。当你有两个或更多的嵌套层级且需要传递数据给子组件时,可以避免"prop drilling"。跨多层组件进行数据传递。
案例分析:主题切换
js
const ThemeContext = React.createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme === 'dark' ? '#333' : '#eee',
color: theme === 'dark' ? '#fff' : '#000'
}}>
我是{theme}主题的按钮
</button>
);
}
以上代码演示了如何利用 useContext
实现跨层级的主题切换功能,无需手动将 props
逐层传递下去。 被ThemeContext.Provider包裹的才可以用到父组件的值,