📚 一、Hooks 概述
🌟 什么是 Hooks
Hooks 是 React 16.8 引入的新特性,它是一种函数编程思想的体现。Hooks 以 use 开头,用于封装 Vue/React 组件的状态和生命周期,让开发者可以"呼之即来",使用起来非常方便。
Hooks 的核心理念是将组件的状态逻辑抽离出来,使组件更加简洁、可维护。通过 Hooks,我们可以在不编写 class 组件的情况下使用 state 以及其他的 React 特性。
🔧 Hooks 的分类
Hooks 可以分为两大类:
- React 内置 Hooks:React 官方提供的一系列常用 Hooks
- 自定义 Hooks:开发者根据业务需求自己封装的 Hooks
🎨 二、React 内置 Hooks 详解
1️⃣ useState Hook
useState 是最基础的 Hook,用于在函数组件中添加状态管理能力。
基本语法
javascript
const [state, setState] = useState(initialValue)
参数说明
initialValue:状态的初始值,可以是任意类型(数字、字符串、对象、数组等)- 也可以是一个函数,用于惰性初始化状态
返回值
返回一个数组,包含两个元素:
- 第一个元素:当前状态的值
- 第二个元素:更新状态的函数
使用示例
javascript
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
)
}
惰性初始化
当初始状态需要通过复杂计算得出时,可以使用函数作为初始值:
javascript
const [todos, setTodos] = useState(() => {
const storedTodos = localStorage.getItem('todos')
return storedTodos ? JSON.parse(storedTodos) : []
})
这种方式可以避免每次渲染都执行复杂的初始化逻辑。
状态更新注意事项
- 直接替换:状态更新是直接替换,而不是合并
javascript
const [user, setUser] = useState({ name: '张三', age: 18 })
// 错误方式
setUser({ age: 19 }) // 会丢失 name 属性
// 正确方式
setUser({ ...user, age: 19 })
-
异步更新:状态更新是异步的,不能立即获取到最新值
-
函数式更新:当新状态依赖于旧状态时,使用函数式更新
javascript
setCount(prevCount => prevCount + 1)
2️⃣ useEffect Hook
useEffect 用于处理副作用操作,如数据获取、订阅、手动修改 DOM 等。
基本语法
javascript
useEffect(() => {
// 副作用代码
return () => {
// 清理函数(可选)
}
}, [dependencies])
参数说明
- 第一个参数:副作用函数,在组件渲染后执行
- 第二个参数:依赖数组,控制副作用何时执行
执行时机
| 依赖数组 | 执行时机 |
|---|---|
| 不提供 | 每次渲染后都执行 |
[] |
只在组件挂载时执行一次 |
[a, b] |
当 a 或 b 变化时执行 |
清理函数
清理函数在组件卸载或下一次副作用执行前执行,主要用于:
- 清除事件监听器
- 清除定时器
- 取消网络请求
- 清除订阅
使用示例:事件监听
javascript
import { useState, useEffect } from 'react'
export default function useMouse() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
const update = (event) => {
console.log('鼠标移动')
setX(event.pageX)
setY(event.pageY)
}
// 组件挂载时,监听 mousemove 事件
window.addEventListener('mousemove', update)
console.log('事件监听已添加')
return () => {
// 组件卸载时,移除 mousemove 事件
// 防止内存泄漏
console.log('清除事件监听')
window.removeEventListener('mousemove', update)
}
}, []) // 空依赖数组,只在挂载时执行一次
return (
<div>
鼠标位置:{x} {y}
</div>
)
}
使用示例:数据持久化
javascript
const STORAGE_KEY = 'todos'
function loadFromStorage() {
const storedTodos = localStorage.getItem(STORAGE_KEY)
return storedTodos ? JSON.parse(storedTodos) : []
}
function saveToStorage(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
export default function useTodos() {
const [todos, setTodos] = useState(() => loadFromStorage())
// 监听 todos 变化,保存到 localStorage
useEffect(() => {
saveToStorage(todos)
}, [todos])
return { todos, setTodos }
}
3️⃣ useContext Hook
useContext 用于在组件树中跨层级传递数据,避免通过 props 一层层传递。
基本语法
javascript
const value = useContext(MyContext)
使用步骤
- 创建 Context
javascript
const MyContext = React.createContext(defaultValue)
- 提供 Context
javascript
<MyContext.Provider value={/* 某个值 */}>
<子组件 />
</MyContext.Provider>
- 消费 Context
javascript
import { useContext } from 'react'
function ChildComponent() {
const value = useContext(MyContext)
return <div>{value}</div>
}
使用示例
javascript
import { createContext, useContext, useState } from 'react'
// 创建 Context
const ThemeContext = createContext()
// 父组件提供 Context
function App() {
const [theme, setTheme] = useState('light')
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Content />
</ThemeContext.Provider>
)
}
// 子组件消费 Context
function Header() {
const { theme, setTheme } = useContext(ThemeContext)
return (
<header className={theme}>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题
</button>
</header>
)
}
4️⃣ useRef Hook
useRef 用于创建一个可变的 ref 对象,其 .current 属性可以被赋值和读取。
基本语法
javascript
const refContainer = useRef(initialValue)
主要用途
- 访问 DOM 元素
javascript
import { useRef, useEffect } from 'react'
function TextInput() {
const inputRef = useRef(null)
useEffect(() => {
inputRef.current.focus()
}, [])
return <input ref={inputRef} type="text" />
}
- 保存可变值(不触发重新渲染)
javascript
function Timer() {
const timerRef = useRef(null)
const start = () => {
timerRef.current = setInterval(() => {
console.log('定时器运行中')
}, 1000)
}
const stop = () => {
clearInterval(timerRef.current)
}
return (
<div>
<button onClick={start}>开始</button>
<button onClick={stop}>停止</button>
</div>
)
}
- 保存上一次的值
javascript
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
📝 三、JavaScript 函数回顾
1️⃣ 普通函数
使用 function 关键字声明的函数。
javascript
function greet(name) {
return `你好,${name}!`
}
console.log(greet('张三')) // 你好,张三!
特点:
- 有函数提升
- 可以作为构造函数使用
this指向调用时的对象
2️⃣ 箭头函数
ES6 引入的函数语法,更简洁。
javascript
const greet = (name) => {
return `你好,${name}!`
}
// 简写形式
const greet = name => `你好,${name}!`
console.log(greet('李四')) // 你好,李四!
特点:
- 没有
this绑定,this继承自外层作用域 - 没有
arguments对象 - 不能作为构造函数使用
- 没有
prototype属性 - 更简洁的语法
3️⃣ 匿名函数
没有函数名的函数,通常作为回调函数使用。
javascript
setTimeout(function() {
console.log('1秒后执行')
}, 1000)
// 箭头函数形式的匿名函数
setTimeout(() => {
console.log('1秒后执行')
}, 1000)
4️⃣ 立即执行函数
定义后立即执行的函数。
javascript
(function() {
console.log('立即执行')
})()
// 箭头函数形式
(() => {
console.log('立即执行')
})()
用途:
- 创建独立作用域,避免变量污染
- 模块化代码
- 初始化配置
5️⃣ 递归函数
函数调用自身。
javascript
function factorial(n) {
if (n <= 1) return 1
return n * factorial(n - 1)
}
console.log(factorial(5)) // 120
使用场景:
- 遍历树形结构
- 计算阶乘、斐波那契数列
- 深度优先搜索
6️⃣ 回调函数
作为参数传递给另一个函数的函数。
javascript
function processData(data, callback) {
// 处理数据
const result = data.map(item => item * 2)
// 调用回调函数
callback(result)
}
processData([1, 2, 3], (result) => {
console.log(result) // [2, 4, 6]
})
在 React 中广泛应用:
javascript
<button onClick={() => handleClick(id)}>
点击我
</button>
7️⃣ 构造函数
用于创建对象的函数。
javascript
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.greet = function() {
console.log(`我是${this.name},今年${this.age}岁`)
}
const person = new Person('王五', 25)
person.greet() // 我是王五,今年25岁
ES6 类语法:
javascript
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
greet() {
console.log(`我是${this.name},今年${this.age}岁`)
}
}
8️⃣ 闭包
函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。
javascript
function createCounter() {
let count = 0
return {
increment: () => {
count++
console.log(count)
},
decrement: () => {
count--
console.log(count)
}
}
}
const counter = createCounter()
counter.increment() // 1
counter.increment() // 2
counter.decrement() // 1
闭包的应用场景:
- 数据私有化
- 柯里化
- 模块模式
- 事件处理程序
在 React Hooks 中的闭包问题:
javascript
function Counter() {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
console.log(count) // 永远打印 0,因为闭包捕获了初始值
}, 1000)
return () => clearInterval(timer)
}, []) // 空依赖数组
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
解决方案:使用函数式更新或添加正确的依赖。
🚀 四、自定义 Hooks
自定义 Hooks 是一个函数,其名称以 use 开头,函数内部可以调用其他 Hooks。
🎯 自定义 Hooks 的优势
- 逻辑复用:将组件间的共享逻辑抽离到自定义 Hook 中
- 关注点分离:UI 组件更简单,只负责 HTML + CSS,好维护
- 复用性好:和组件一样,是前端团队的核心资产
- 业务逻辑更简单:好测试
📦 案例 1:useMouse Hook
监听鼠标位置的自定义 Hook。
javascript
import { useState, useEffect } from 'react'
export default function useMouse() {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
useEffect(() => {
const update = (event) => {
console.log('鼠标移动')
setX(event.pageX)
setY(event.pageY)
}
// 组件挂载时,监听 mousemove 事件
window.addEventListener('mousemove', update)
console.log('事件监听已添加')
return () => {
// 组件卸载时,移除 mousemove 事件
// 防止内存泄漏
console.log('清除事件监听')
window.removeEventListener('mousemove', update)
}
}, [])
return { x, y }
}
使用方式:
javascript
import useMouse from './hooks/useMouse'
function App() {
const { x, y } = useMouse()
return (
<div>
鼠标位置:{x} {y}
</div>
)
}
📦 案例 2:useTodos Hook
完整的待办事项管理 Hook。
javascript
import { useState, useEffect } from 'react'
const STORAGE_KEY = 'todos'
// 从 localStorage 加载 todos
function loadFromStorage() {
const storedTodos = localStorage.getItem(STORAGE_KEY)
return storedTodos ? JSON.parse(storedTodos) : []
}
// 保存 todos 到 localStorage
function saveToStorage(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
export default function useTodos() {
// useState 接收函数计算同步
const [todos, setTodos] = useState(() => loadFromStorage())
// 监听 todos 变化,保存到 localStorage
useEffect(() => {
saveToStorage(todos)
}, [todos])
// 添加 todo
const addTodo = (text) => {
text = text.trim()
if (text === '') {
return
}
setTodos([
...todos,
{
id: Date.now(),
text: text,
completed: false
}
])
}
// 删除 todo
const deleteTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id))
}
// 切换 todo 完成状态
const toggleTodo = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id === id) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
)
}
return {
todos,
addTodo,
deleteTodo,
toggleTodo
}
}
🎨 组件实现
TodoInput 组件
javascript
import { useState } from 'react'
export default function TodoInput({ onAddTodo }) {
const [text, setText] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (!text.trim()) return
onAddTodo(text.trim())
setText('')
}
return (
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={e => setText(e.target.value)}
/>
<button type="submit">添加</button>
</form>
)
}
TodoItem 组件
javascript
export default function TodoItem({ todo, onDeleteTodo, onToggleTodo }) {
return (
<li className='todo-item'>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggleTodo(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => onDeleteTodo(todo.id)}>删除</button>
</li>
)
}
TodoList 组件
javascript
import TodoItem from './TodoItem.jsx'
export default function TodoList({ todos, onDeleteTodo, onToggleTodo }) {
return (
<ul className='todo-list'>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onDeleteTodo={onDeleteTodo}
onToggleTodo={onToggleTodo}
/>
))}
</ul>
)
}
App 主组件
javascript
import useTodos from './hooks/useTodos.js'
import TodoList from './components/TodoList.jsx'
import TodoInput from './components/TodoInput.jsx'
export default function App() {
const {
todos,
addTodo,
deleteTodo,
toggleTodo,
} = useTodos()
return (
<>
<TodoInput onAddTodo={addTodo} />
{todos.length > 0 ? (
<TodoList
todos={todos}
onDeleteTodo={deleteTodo}
onToggleTodo={toggleTodo}
/>
) : (
<div>暂无待办事项</div>
)}
</>
)
}
⚠️ 五、内存泄漏与清理
🔍 什么是内存泄漏
内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
🐌 在 React 中的常见场景
- 未清除的事件监听器
javascript
// 错误示例
useEffect(() => {
window.addEventListener('mousemove', update)
// 缺少清理函数
}, [])
- 未清除的定时器
javascript
// 错误示例
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器')
}, 1000)
// 缺少清理函数
}, [])
- 未取消的网络请求
javascript
// 错误示例
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data))
// 组件卸载后,请求可能仍在进行
}, [])
✅ 正确的清理方式
事件监听器清理
javascript
useEffect(() => {
const update = (event) => {
setX(event.pageX)
setY(event.pageY)
}
window.addEventListener('mousemove', update)
return () => {
window.removeEventListener('mousemove', update)
}
}, [])
定时器清理
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行')
}, 1000)
return () => {
clearInterval(timer)
}
}, [])
网络请求清理
javascript
useEffect(() => {
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err)
}
})
return () => {
controller.abort()
}
}, [])
🎯 useEffect 清理函数执行时机
清理函数在以下时机执行:
- 组件卸载时:组件从 DOM 中移除时
- 下一次副作用执行前:当依赖数组变化,新的副作用执行前
javascript
useEffect(() => {
console.log('副作用执行')
return () => {
console.log('清理函数执行')
}
}, [count])
执行顺序:
- 组件挂载:副作用执行
- count 变化:清理函数执行 → 副作用执行
- 组件卸载:清理函数执行
🎓 六、Hooks 使用规则
📏 两条黄金规则
- 只在函数最顶层调用 Hooks
- 不要在循环、条件判断或嵌套函数中调用 Hooks
- 确保 Hooks 在每次渲染时都以相同的顺序被调用
javascript
// ❌ 错误示例
if (count > 0) {
useEffect(() => {
// ...
}, [])
}
// ✅ 正确示例
useEffect(() => {
if (count > 0) {
// ...
}
}, [count])
- 只在 React 函数中调用 Hooks
- 在 React 函数组件中调用 Hooks
- 在自定义 Hooks 中调用 Hooks
javascript
// ❌ 错误示例
function regularFunction() {
const [count, setCount] = useState(0)
}
// ✅ 正确示例
function Component() {
const [count, setCount] = useState(0)
}
🔧 ESLint 插件
使用 eslint-plugin-react-hooks 来强制执行这些规则:
javascript
{
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
📊 七、常用内置 Hooks 补充
useMemo Hook
用于缓存计算结果,避免不必要的重复计算。
javascript
import { useMemo } from 'react'
function ExpensiveComponent({ items }) {
const sortedItems = useMemo(() => {
console.log('计算排序')
return items.sort((a, b) => a.value - b.value)
}, [items])
return <div>{sortedItems.map(...)}</div>
}
useCallback Hook
用于缓存函数,避免子组件不必要的重新渲染。
javascript
import { useCallback } from 'react'
function ParentComponent() {
const handleClick = useCallback(() => {
console.log('点击')
}, [])
return <ChildComponent onClick={handleClick} />
}
useReducer Hook
用于复杂的状态管理,类似 Redux。
javascript
import { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div>
<p>{state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>增加</button>
<button onClick={() => dispatch({ type: 'decrement' })}>减少</button>
</div>
)
}
🎯 八、项目实战总结
📁 项目结构
css
hooks-demo/
├── src/
│ ├── components/
│ │ ├── TodoInput.jsx
│ │ ├── TodoItem.jsx
│ │ └── TodoList.jsx
│ ├── hooks/
│ │ ├── useMouse.js
│ │ └── useTodos.js
│ ├── App.jsx
│ ├── main.jsx
│ └── index.css
└── package.json
🔄 数据流
scss
App (使用 useTodos)
├── TodoInput (接收 onAddTodo)
└── TodoList (接收 todos, onDeleteTodo, onToggleTodo)
└── TodoItem (接收 todo, onDeleteTodo, onToggleTodo)
💡 核心要点
- 自定义 Hooks 封装业务逻辑,使组件更简洁
- useEffect 的清理函数防止内存泄漏
- useState 的惰性初始化提高性能
- localStorage 持久化数据
- 组件化开发,关注点分离
🚀 九、最佳实践
✨ 命名规范
- 自定义 Hooks 必须以
use开头 - 组件名使用 PascalCase
- 函数名使用 camelCase
🎨 组件设计
- 单一职责:每个组件只做一件事
- Props 最小化:只传递必要的 props
- 组合优于继承:使用组合构建复杂组件
🔧 Hooks 设计
- 关注点分离:将相关逻辑放在一个 Hook 中
- 返回对象:便于解构使用
- 提供清理函数:避免副作用导致的内存泄漏
📊 性能优化
- 使用 useMemo 缓存计算结果
- 使用 useCallback 缓存函数
- 合理使用依赖数组
- 避免不必要的渲染
🎊 十、总结
React Hooks 是 React 开发的核心特性,它通过函数式编程思想,让我们能够更优雅地管理组件状态和副作用。
核心要点回顾:
- useState:管理组件状态
- useEffect:处理副作用,包括清理函数防止内存泄漏
- useContext:跨层级传递数据
- useRef:访问 DOM 和保存可变值
- 自定义 Hooks:逻辑复用,提高代码可维护性
通过合理使用 Hooks,我们可以编写出更简洁、更易维护、更易测试的 React 代码。Hooks 让函数组件拥有了类组件的所有能力,同时避免了类组件的复杂性和 this 绑定问题。
在实际开发中,遵循 Hooks 的使用规则,合理设计自定义 Hooks,充分利用 React 生态中的各种 Hooks,将大大提升开发效率和代码质量。