一、Class组件
1. Class组件基础
import React, { Component } from 'react'
class MyComponent extends Component {
constructor(props) {
super(props)
// 初始化状态
this.state = {
count: 0,
user: null
}
// 绑定this
this.handleClick = this.handleClick.bind(this)
}
// 实例方法
handleClick() {
this.setState({ count: this.state.count + 1 })
}
// 箭头函数自动绑定this
handleReset = () => {
this.setState({ count: 0 })
}
render() {
return (
<div>
<p>计数:{this.state.count}</p>
<button onClick={this.handleClick}>增加</button>
<button onClick={this.handleReset}>重置</button>
</div>
)
}
}
2. 生命周期深度剖析
1️⃣ 挂载阶段(Mounting)
class LifecycleDemo extends Component {
// 1. constructor - 初始化
constructor(props) {
super(props)
console.log('1. constructor')
this.state = { count: 0 }
// 用途:初始化state、绑定方法
// 注意:不要在这里调用setState
}
// 2. static getDerivedStateFromProps - 根据props更新state
static getDerivedStateFromProps(props, state) {
console.log('2. getDerivedStateFromProps')
// 返回值会合并到state,返回null表示不更新
if (props.initialCount !== state.count) {
return { count: props.initialCount }
}
return null
}
// 3. render - 渲染
render() {
console.log('3. render')
return <div>{this.state.count}</div>
}
// 4. componentDidMount - 挂载完成
componentDidMount() {
console.log('4. componentDidMount')
// ✅ 适合做的操作:
// - 发送网络请求
// - 添加订阅
// - 操作DOM
// - 设置定时器
this.timer = setInterval(() => {
this.setState({ count: this.state.count + 1 })
}, 1000)
}
// 清理定时器
componentWillUnmount() {
clearInterval(this.timer)
}
}
2️⃣ 更新阶段(Updating)
class UpdateDemo extends Component {
state = { count: 0 }
// 1. static getDerivedStateFromProps - props变化时调用
static getDerivedStateFromProps(props, state) {
console.log('更新-1: getDerivedStateFromProps')
return null
}
// 2. shouldComponentUpdate - 性能优化关键!
shouldComponentUpdate(nextProps, nextState) {
console.log('更新-2: shouldComponentUpdate')
// 返回false可阻止重新渲染
if (nextState.count === this.state.count) {
return false // 状态没变,不更新
}
if (nextProps.id === this.props.id) {
return false // 关键props没变,不更新
}
return true // 需要更新
}
// 3. render - 重新渲染
render() {
console.log('更新-3: render')
return (
<div>
<ChildComponent count={this.state.count} />
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
更新
</button>
</div>
)
}
// 4. getSnapshotBeforeUpdate - 获取更新前信息
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('更新-4: getSnapshotBeforeUpdate')
// 可以获取更新前的DOM信息
if (prevState.count < this.state.count) {
return { scrollPosition: window.scrollY }
}
return null
}
// 5. componentDidUpdate - 更新完成
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('更新-5: componentDidUpdate')
// 根据snapshot恢复滚动位置
if (snapshot) {
window.scrollTo(0, snapshot.scrollPosition)
}
// 典型的网络请求场景
if (prevProps.id !== this.props.id) {
this.fetchData(this.props.id)
}
}
fetchData = (id) => {
// 发送请求
}
}
3️⃣ 卸载阶段(Unmounting)
class UnmountDemo extends Component {
componentDidMount() {
// 添加事件监听
window.addEventListener('resize', this.handleResize)
// 建立WebSocket连接
this.socket = new WebSocket('ws://example.com')
// 设置定时器
this.timer = setInterval(this.tick, 1000)
}
// componentWillUnmount - 清理工作
componentWillUnmount() {
console.log('组件即将卸载')
// ✅ 必须清理的内容:
// 1. 移除事件监听
window.removeEventListener('resize', this.handleResize)
// 2. 关闭WebSocket连接
this.socket.close()
// 3. 清除定时器
clearInterval(this.timer)
// 4. 取消网络请求
this.abortController?.abort()
// 5. 清理订阅
this.subscription.unsubscribe()
}
handleResize = () => {
console.log('窗口大小改变了')
}
tick = () => {
console.log('tick')
}
}
3. 性能优化技巧
1️⃣ PureComponent 自动优化
import React, { PureComponent } from 'react'
// PureComponent 自动实现shouldComponentUpdate的浅比较
class OptimizedList extends PureComponent {
state = {
items: []
}
// 注意:直接修改数组不会触发更新!
addItem = () => {
// ❌ 错误:直接push不会触发更新
// this.state.items.push(newItem)
// this.setState({ items: this.state.items })
// ✅ 正确:创建新数组
this.setState(prevState => ({
items: [...prevState.items, { id: Date.now(), text: 'New' }]
}))
}
render() {
console.log('OptimizedList render')
return (
<ul>
{this.state.items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
)
}
}
2️⃣ 手动控制更新
class ManualOptimize extends Component {
state = { data: {}, visible: true }
// 精细化控制更新条件
shouldComponentUpdate(nextProps, nextState) {
// 深度比较对象
if (!shallowEqual(this.state.data, nextState.data)) {
return true
}
// 只有visible变化才更新
if (this.state.visible !== nextState.visible) {
return true
}
// 检查特定props
if (this.props.id !== nextProps.id) {
return true
}
return false
}
// 批量更新状态
handleBatchUpdate = () => {
// React会自动批量处理setState
this.setState({ visible: !this.state.visible })
this.setState({ data: { name: 'test' } })
}
render() {
return <div>{this.state.data.name}</div>
}
}
// 浅比较工具函数
function shallowEqual(obj1, obj2) {
if (obj1 === obj2) return true
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') return false
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
if (keys1.length !== keys2.length) return false
return keys1.every(key => obj1[key] === obj2[key])
}
3️⃣ 避免不必要的渲染
class AvoidReRender extends Component {
// 1. 绑定this的最佳实践
constructor(props) {
super(props)
// ❌ 避免:每次render都会创建新函数
// this.handleClick = this.handleClick.bind(this)
}
// ✅ 推荐:使用类属性箭头函数
handleClick = () => {
console.log('clicked')
}
// 2. 缓存复杂计算结果
getProcessedData() {
const { data } = this.props
// 如果计算复杂,可以考虑缓存结果
if (this.cachedData && this.cachedData.input === data) {
return this.cachedData.result
}
const result = expensiveComputation(data)
this.cachedData = { input: data, result }
return result
}
render() {
// 3. 控制子组件渲染
return (
<div>
{/* 使用key强制重新创建组件 */}
{this.state.reset && <Child key={Date.now()} />}
{/* 使用条件渲染避免隐藏组件 */}
{this.state.visible && <ExpensiveComponent />}
</div>
)
}
}
4. 高级用法与模式
1️⃣ 错误边界(Error Boundaries)
class ErrorBoundary extends Component {
state = { hasError: false, error: null }
// 捕获后代组件错误
static getDerivedStateFromError(error) {
// 更新state,下次渲染显示降级UI
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// 记录错误到监控服务
console.error('组件错误:', error, errorInfo)
// 可以发送到错误追踪服务
// logErrorToService(error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>出错了!</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
)
}
return this.props.children
}
}
// 使用方式
<ErrorBoundary>
<MyComponent />
</ErrorBoundary>
2️⃣ Render Props 模式
class MouseTracker extends Component {
state = { x: 0, y: 0 }
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
// 使用
<MouseTracker render={({ x, y }) => (
<h1>鼠标位置:{x}, {y}</h1>
)} />
3️⃣ 高阶组件(HOC)
// 日志HOC
function withLogger(WrappedComponent) {
return class extends Component {
componentDidMount() {
console.log(`组件 ${WrappedComponent.name} 已挂载`)
}
componentWillUnmount() {
console.log(`组件 ${WrappedComponent.name} 将卸载`)
}
render() {
return <WrappedComponent {...this.props} />
}
}
}
// 权限控制HOC
function withAuth(WrappedComponent) {
return class extends Component {
render() {
const { isAuthenticated, ...rest } = this.props
if (!isAuthenticated) {
return <div>请先登录</div>
}
return <WrappedComponent {...rest} />
}
}
}
// 组合使用
const EnhancedComponent = withLogger(withAuth(MyComponent))
5. 原理解析
1️⃣ setState是同步还是异步
class SetStateDemo extends Component {
state = { count: 0 }
handleClick = () => {
// 在React事件处理中:异步
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 还是旧值
// 在setTimeout中:同步
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // 已更新
}, 0)
}
// 获取最新值的方式
handleCorrect = () => {
this.setState((prevState, props) => {
console.log('最新count:', prevState.count + 1)
return { count: prevState.count + 1 }
})
}
}
2️⃣ 为什么需要super(props)
class WhySuper extends Component {
constructor(props) {
super(props) // 必须调用,否则this未初始化
console.log(this.props) // 可以访问props
}
// 不写constructor也可以
// 会自动调用super(props)
}
6. 实战技巧汇总
1. 防抖与节流
class SearchComponent extends Component {
state = { keyword: '', results: [] }
// 使用lodash的debounce
debouncedSearch = _.debounce(this.search, 500)
search = async (keyword) => {
const results = await fetch(`/api/search?q=${keyword}`)
this.setState({ results })
}
handleChange = (e) => {
const keyword = e.target.value
this.setState({ keyword })
this.debouncedSearch(keyword)
}
componentWillUnmount() {
this.debouncedSearch.cancel() // 取消未执行的搜索
}
}
2. 获取最新props/state
class LatestRef extends Component {
state = { count: 0 }
// 使用ref保存最新值
countRef = React.createRef()
componentDidUpdate() {
this.countRef.current = this.state.count
}
handleAsync = () => {
setTimeout(() => {
// 使用ref获取最新值
console.log('最新count:', this.countRef.current)
}, 1000)
}
}
Class组件的核心要点:
- 生命周期清晰:挂载、更新、卸载三阶段
- 性能优化手段丰富:PureComponent、shouldComponentUpdate
- 状态管理强大:setState的合并与批量更新
- 逻辑复用模式:HOC、Render Props
二、React Hooks
1. Hooks 基础
// ❌ Class组件的痛点
class Problems extends Component {
// 1. 逻辑分散:相关代码被拆分到不同生命周期
componentDidMount() {
// 订阅逻辑
// 数据获取逻辑
// DOM操作逻辑
}
componentDidUpdate() {
// 订阅逻辑又要写一遍
// 数据获取逻辑又要写一遍
}
// 2. this指向问题
handleClick() {
// 需要bind(this)
}
// 3. 逻辑复用困难(HOC嵌套地狱)
render() {
return (
<WithAuth>
<WithLogger>
<WithTheme>
<MyComponent />
</WithTheme>
</WithLogger>
</WithAuth>
)
}
}
// ✅ Hooks解决方案
function Solution() {
// 相关逻辑可以放在一起
useEffect(() => {
// 订阅逻辑
// 返回清理函数
return () => {}
}, [])
useEffect(() => {
// 数据获取逻辑
}, [])
// 没有this困扰
const handleClick = () => {}
// 自定义Hook轻松复用逻辑
const { user, loading } = useUser()
const theme = useTheme()
return <div />
}
2. 核心Hooks详解
1️⃣ useState - 状态管理
import React, { useState } from 'react'
function Counter() {
// 基础用法
const [count, setCount] = useState(0)
// 函数式更新
const handleClick = () => {
setCount(prevCount => prevCount + 1)
}
// 复杂状态
const [user, setUser] = useState({
name: '',
age: 0,
hobbies: []
})
// 更新复杂状态
const updateName = (name) => {
setUser(prevUser => ({
...prevUser,
name
}))
}
// 惰性初始化(适合计算复杂的初始值)
const [data, setData] = useState(() => {
const expensiveData = computeExpensiveValue(props.initial)
return expensiveData
})
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+</button>
</div>
)
}
2️⃣ useEffect - 副作用处理
function EffectDemo({ userId }) {
const [data, setData] = useState(null)
const [count, setCount] = useState(0)
// 1. 组件挂载时执行(componentDidMount)
useEffect(() => {
console.log('组件挂载了')
// 适合:数据获取、订阅设置
return () => {
console.log('组件卸载了')
// 清理:取消订阅、清除定时器
}
}, []) // 空依赖数组
// 2. 依赖更新时执行(componentDidUpdate)
useEffect(() => {
console.log('userId变化了:', userId)
fetchUserData(userId)
// 可以返回清理函数
return () => {
console.log('清理上一个userId的效果')
cancelRequest()
}
}, [userId]) // 依赖userId
// 3. 每次渲染后执行
useEffect(() => {
console.log('组件更新了')
document.title = `点击了${count}次`
}) // 不传依赖数组
// 4. 多个useEffect按顺序执行
useEffect(() => {
console.log('第一个effect')
}, [])
useEffect(() => {
console.log('第二个effect')
}, [])
// 5. 条件执行
useEffect(() => {
if (count > 5) {
console.log('count大于5了')
}
}, [count])
// 6. 防抖处理
useEffect(() => {
const timer = setTimeout(() => {
console.log('搜索:', userId)
}, 500)
return () => clearTimeout(timer)
}, [userId])
return <div>{data?.name}</div>
}
3️⃣ useContext - 跨组件通信
import React, { useContext, createContext } from 'react'
// 创建Context
const ThemeContext = createContext()
const UserContext = createContext()
// 1. 基础用法
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 2. 消费Context
function ThemedButton() {
// 直接使用useContext
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button
onClick={toggleTheme}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
切换主题
</button>
)
}
// 3. 多个Context使用
function Profile() {
const theme = useContext(ThemeContext)
const user = useContext(UserContext)
return (
<div style={{ color: theme === 'light' ? '#000' : '#fff' }}>
用户:{user.name}
</div>
)
}
// 4. 封装自定义Hook
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme必须在ThemeProvider内使用')
}
return context
}
// 使用
function App() {
return (
<ThemeProvider>
<UserContext.Provider value={{ name: '张三' }}>
<ThemedButton />
<Profile />
</UserContext.Provider>
</ThemeProvider>
)
}
4️⃣ useReducer - 复杂状态逻辑
import React, { useReducer } from 'react'
// 1. 定义reducer
const initialState = {
count: 0,
loading: false,
error: null
}
function reducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 }
case 'DECREMENT':
return { ...state, count: state.count - 1 }
case 'RESET':
return { ...state, count: 0 }
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.error }
default:
return state
}
}
// 2. 使用useReducer
function CounterWithReducer() {
const [state, dispatch] = useReducer(reducer, initialState)
// 复杂操作
const handleAsyncIncrement = async () => {
dispatch({ type: 'FETCH_START' })
try {
const response = await fetch('/api/increment')
const data = await response.json()
dispatch({ type: 'FETCH_SUCCESS', payload: data })
} catch (error) {
dispatch({ type: 'FETCH_ERROR', error: error.message })
}
}
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
<button onClick={handleAsyncIncrement}>异步增加</button>
{state.loading && <p>加载中...</p>}
{state.error && <p>错误:{state.error}</p>}
</div>
)
}
// 3. 结合useContext实现小型Redux
const StoreContext = createContext()
function StoreProvider({ children }) {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<StoreContext.Provider value={{ state, dispatch }}>
{children}
</StoreContext.Provider>
)
}
function useStore() {
return useContext(StoreContext)
}
5️⃣ useMemo - 缓存计算结果
import React, { useMemo, useState } from 'react'
function MemoDemo({ list, filterText }) {
const [count, setCount] = useState(0)
// 1. 缓存复杂计算
const filteredList = useMemo(() => {
console.log('计算过滤列表...')
return list.filter(item =>
item.name.includes(filterText)
)
}, [list, filterText]) // 只有依赖变化时才重新计算
// 2. 缓存对象引用
const config = useMemo(() => ({
color: count > 5 ? 'red' : 'blue',
size: 'large'
}), [count > 5]) // 依赖于布尔值
// 3. 避免子组件不必要的重渲染
return (
<div>
<button onClick={() => setCount(count + 1)}>
点击:{count}
</button>
<ExpensiveList
list={filteredList}
config={config}
/>
</div>
)
}
// 配合React.memo使用
const ExpensiveList = React.memo(({ list, config }) => {
console.log('ExpensiveList渲染')
return (
<ul style={{ color: config.color }}>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
})
6️⃣ useCallback - 缓存函数
import React, { useState, useCallback } from 'react'
function CallbackDemo() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// 1. 缓存函数引用
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1)
}, []) // 空依赖,函数永远不会变
// 2. 依赖特定值
const handleAddTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text }])
}, []) // setTodos是稳定的,所以依赖为空
// 3. 依赖count
const handleAlert = useCallback(() => {
alert(`当前count: ${count}`)
}, [count]) // count变化时重新创建
// 4. 配合子组件使用
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
{/* 这些子组件都不会因为父组件重渲染而重新创建函数 */}
<ChildButton onClick={handleIncrement}>
增加
</ChildButton>
<ChildButton onClick={handleAlert}>
提示count
</ChildButton>
</div>
)
}
const ChildButton = React.memo(({ onClick, children }) => {
console.log(`Button ${children} 渲染`)
return <button onClick={onClick}>{children}</button>
})
7️⃣ useRef - 引用和可变值
import React, { useRef, useEffect, useState } from 'react'
function RefDemo() {
// 1. DOM引用
const inputRef = useRef(null)
const divRef = useRef(null)
// 2. 可变值(不会触发重渲染)
const countRef = useRef(0)
const [count, setCount] = useState(0)
// 3. 存储定时器ID
const timerRef = useRef(null)
// 4. 存储上一次的值
const prevCountRef = useRef()
useEffect(() => {
// 自动聚焦
inputRef.current?.focus()
}, [])
useEffect(() => {
// 获取DOM尺寸
const width = divRef.current?.offsetWidth
console.log('div宽度:', width)
})
useEffect(() => {
// 存储上一次的count
prevCountRef.current = count
}, [count])
const handleClick = () => {
// 更新ref不会触发重渲染
countRef.current += 1
console.log('ref count:', countRef.current)
// state更新会触发重渲染
setCount(count + 1)
}
const startTimer = () => {
timerRef.current = setInterval(() => {
console.log('timer')
}, 1000)
}
const stopTimer = () => {
clearInterval(timerRef.current)
}
useEffect(() => {
// 清理定时器
return () => clearInterval(timerRef.current)
}, [])
return (
<div ref={divRef}>
<input ref={inputRef} placeholder="自动聚焦" />
<p>当前count: {count}</p>
<p>上一次count: {prevCountRef.current}</p>
<p>ref count: {countRef.current}</p>
<button onClick={handleClick}>更新</button>
<button onClick={startTimer}>开始计时</button>
<button onClick={stopTimer}>停止计时</button>
</div>
)
}
8️⃣ useImperativeHandle - 自定义暴露方法
import React, {
useRef,
useImperativeHandle,
forwardRef
} from 'react'
// 子组件
const CustomInput = forwardRef((props, ref) => {
const inputRef = useRef(null)
// 自定义暴露给父组件的方法
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
},
blur: () => {
inputRef.current.blur()
},
setValue: (value) => {
inputRef.current.value = value
},
getValue: () => {
return inputRef.current.value
},
validate: () => {
const value = inputRef.current.value
return value.length >= 6
}
}), []) // 依赖数组
return (
<input
ref={inputRef}
{...props}
placeholder="请输入..."
/>
)
})
// 父组件
function Parent() {
const inputRef = useRef(null)
const handleClick = () => {
// 调用子组件暴露的方法
inputRef.current.focus()
inputRef.current.setValue('Hello')
if (inputRef.current.validate()) {
console.log('验证通过')
}
}
return (
<div>
<CustomInput ref={inputRef} />
<button onClick={handleClick}>
操作输入框
</button>
</div>
)
}
9️⃣ useLayoutEffect - 同步执行副作用
import React, { useState, useLayoutEffect, useEffect } from 'react'
function LayoutEffectDemo() {
const [width, setWidth] = useState(0)
const [height, setHeight] = useState(0)
const ref = useRef(null)
// useEffect 异步执行
useEffect(() => {
console.log('useEffect: DOM更新后异步执行')
})
// useLayoutEffect 同步执行
useLayoutEffect(() => {
console.log('useLayoutEffect: DOM更新后、浏览器绘制前同步执行')
// 适合:需要读取DOM布局并立即同步修改
if (ref.current) {
const { width, height } = ref.current.getBoundingClientRect()
// 如果宽度太小,设置一个最小宽度
if (width < 100) {
ref.current.style.minWidth = '100px'
}
setWidth(width)
setHeight(height)
}
}, []) // 空依赖,只在挂载时执行
return (
<div>
<div
ref={ref}
style={{
width: '50%',
padding: '20px',
background: '#f0f0f0'
}}
>
测量我的尺寸
</div>
<p>宽度:{width}px</p>
<p>高度:{height}px</p>
</div>
)
}
🔟 useDebugValue - 自定义Hook调试
import React, { useState, useEffect, useDebugValue } from 'react'
// 自定义Hook
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
// 在React DevTools中显示调试信息
useDebugValue(isOnline ? '在线' : '离线')
useDebugValue('网络状态Hook')
// 也可以根据条件显示
useDebugValue({ isOnline }, (value) => {
return `用户当前${value.isOnline ? '在线' : '离线'}`
})
return isOnline
}
// 使用
function App() {
const isOnline = useOnlineStatus()
return <div>当前状态:{isOnline ? '在线' : '离线'}</div>
}
🔟 useInsertionEffect - CSS-in-JS
import { useInsertionEffect } from 'react'
function Component() {
useInsertionEffect(() => {
// 在DOM变更前插入样式
console.log('样式插入阶段')
return () => {
// 清理样式
console.log('清理样式')
}
}, [])
return <div />
}
执行时机:
function TimingComparison() {
// 1. useInsertionEffect - 最先执行
useInsertionEffect(() => {
console.log('1. useInsertionEffect: DOM变更前,同步执行')
// 适合:插入<style>、动态样式计算
})
// 2. useLayoutEffect - 其次执行
useLayoutEffect(() => {
console.log('2. useLayoutEffect: DOM变更后,浏览器绘制前')
// 适合:读取DOM布局,同步重绘
})
// 3. useEffect - 最后执行
useEffect(() => {
console.log('3. useEffect: 浏览器绘制后,异步执行')
// 适合:数据获取、订阅、副作用
})
return <div>检查控制台执行顺序</div>
}
// 执行顺序:
// 1. useInsertionEffect
// 2. useLayoutEffect
// 3. useEffect
3. 自定义Hooks
1️⃣ 数据请求Hook
import { useState, useEffect } from 'react'
function useFetch(url, options = {}) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
const abortController = new AbortController()
const fetchData = async () => {
try {
setLoading(true)
const response = await fetch(url, {
...options,
signal: abortController.signal
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
setData(result)
setError(null)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message)
setData(null)
}
} finally {
setLoading(false)
}
}
fetchData()
return () => {
abortController.abort()
}
}, [url, JSON.stringify(options)])
return { data, loading, error }
}
// 使用
function UserProfile({ userId }) {
const { data, loading, error } = useFetch(
`https://api.example.com/users/${userId}`
)
if (loading) return <div>加载中...</div>
if (error) return <div>错误:{error}</div>
return <div>{data?.name}</div>
}
2️⃣ 表单处理Hook
import { useState, useCallback } from 'react'
function useForm(initialValues = {}, validate = null) {
const [values, setValues] = useState(initialValues)
const [errors, setErrors] = useState({})
const [touched, setTouched] = useState({})
// 处理输入变化
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}))
// 实时验证
if (validate) {
const validationErrors = validate({ [name]: value })
setErrors(prev => ({ ...prev, ...validationErrors }))
}
}, [validate])
// 处理失焦
const handleBlur = useCallback((e) => {
const { name } = e.target
setTouched(prev => ({ ...prev, [name]: true }))
}, [])
// 重置表单
const resetForm = useCallback(() => {
setValues(initialValues)
setErrors({})
setTouched({})
}, [initialValues])
// 设置多个值
const setFormValues = useCallback((newValues) => {
setValues(prev => ({ ...prev, ...newValues }))
}, [])
return {
values,
errors,
touched,
handleChange,
handleBlur,
resetForm,
setFormValues,
isValid: Object.keys(errors).length === 0
}
}
// 使用
function LoginForm() {
const validate = (values) => {
const errors = {}
if (!values.email) {
errors.email = '请输入邮箱'
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = '邮箱格式不正确'
}
if (!values.password) {
errors.password = '请输入密码'
} else if (values.password.length < 6) {
errors.password = '密码至少6位'
}
return errors
}
const {
values,
errors,
touched,
handleChange,
handleBlur,
isValid
} = useForm({
email: '',
password: ''
}, validate)
const handleSubmit = (e) => {
e.preventDefault()
if (isValid) {
console.log('提交:', values)
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
placeholder="邮箱"
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
onBlur={handleBlur}
placeholder="密码"
/>
{touched.password && errors.password && (
<span style={{ color: 'red' }}>{errors.password}</span>
)}
</div>
<button type="submit" disabled={!isValid}>
登录
</button>
</form>
)
}
3️⃣ 无限滚动Hook
import { useState, useEffect, useCallback, useRef } from 'react'
function useInfiniteScroll(fetchMore, hasMore) {
const [loading, setLoading] = useState(false)
const observerRef = useRef()
const lastElementRef = useCallback(
node => {
if (loading) return
if (observerRef.current) observerRef.current.disconnect()
observerRef.current = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasMore) {
setLoading(true)
fetchMore().finally(() => setLoading(false))
}
})
if (node) observerRef.current.observe(node)
},
[loading, hasMore, fetchMore]
)
return { lastElementRef, loading }
}
// 使用
function PostList() {
const [posts, setPosts] = useState([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const fetchMorePosts = useCallback(async () => {
const response = await fetch(`/api/posts?page=${page}`)
const newPosts = await response.json()
setPosts(prev => [...prev, ...newPosts])
setPage(prev => prev + 1)
setHasMore(newPosts.length > 0)
}, [page])
const { lastElementRef, loading } = useInfiniteScroll(
fetchMorePosts,
hasMore
)
return (
<div>
{posts.map((post, index) => (
<div
key={post.id}
ref={index === posts.length - 1 ? lastElementRef : null}
>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
{loading && <div>加载中...</div>}
</div>
)
}
4️⃣ 本地存储Hook
import { useState, useEffect } from 'react'
function useLocalStorage(key, initialValue) {
// 获取初始值
const readValue = () => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error)
return initialValue
}
}
const [storedValue, setStoredValue] = useState(readValue)
// 保存到localStorage
const setValue = (value) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error)
}
}
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === key) {
setStoredValue(e.newValue ? JSON.parse(e.newValue) : initialValue)
}
}
window.addEventListener('storage', handleStorageChange)
return () => window.removeEventListener('storage', handleStorageChange)
}, [key, initialValue])
return [storedValue, setValue]
}
// 使用
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [user, setUser] = useLocalStorage('user', null)
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
<button onClick={() => setUser({ name: '张三', age: 18 })}>
设置用户
</button>
</div>
)
}
4. 性能优化技巧
1️⃣ 避免不必要的重渲染
function OptimizedComponent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// ✅ 使用useCallback缓存函数
const handleIncrement = useCallback(() => {
setCount(c => c + 1)
}, [])
// ✅ 使用useMemo缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(count)
}, [count])
// ✅ 使用useMemo缓存对象
const style = useMemo(() => ({
color: count > 5 ? 'red' : 'blue',
fontSize: '16px'
}), [count > 5])
// ✅ 使用useCallback配合setState的函数式更新
const handleAddTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text }])
}, [])
return (
<div style={style}>
<p>Count: {count}</p>
<p>计算值: {expensiveValue}</p>
<button onClick={handleIncrement}>增加</button>
{/* ✅ 子组件用React.memo包裹 */}
<ExpensiveChild
value={expensiveValue}
style={style}
onAction={handleAddTodo}
/>
</div>
)
}
const ExpensiveChild = React.memo(({ value, style, onAction }) => {
console.log('子组件渲染')
return <div style={style}>{value}</div>
})
2️⃣ 懒加载和代码分割
import React, { Suspense, lazy } from 'react'
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'))
const ChartComponent = lazy(() => import('./ChartComponent'))
function LazyLoadDemo() {
const [showChart, setShowChart] = useState(false)
return (
<div>
<Suspense fallback={<div>加载中...</div>}>
{/* 按需加载 */}
<HeavyComponent />
{showChart && (
<Suspense fallback={<div>图表加载中...</div>}>
<ChartComponent />
</Suspense>
)}
<button onClick={() => setShowChart(true)}>
显示图表
</button>
</Suspense>
</div>
)
}
3️⃣ 虚拟列表优化
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList({ items }) {
const parentRef = useRef()
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50, // 每行高度
})
return (
<div
ref={parentRef}
style={{
height: '400px',
overflow: 'auto'
}}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`
}}
>
{items[virtualItem.index].name}
</div>
))}
</div>
</div>
)
}
5. Hooks使用规则
1️⃣ 只在最顶层使用Hooks
// ❌ 错误:在条件语句中使用
function BadExample() {
if (condition) {
const [count, setCount] = useState(0) // 禁止!
}
for (let i = 0; i < 10; i++) {
useEffect(() => {}) // 禁止!
}
function handleClick() {
const [state, setState] = useState() // 禁止!
}
}
// ✅ 正确:总是在顶层使用
function GoodExample() {
const [count, setCount] = useState(0) // ✅ 顶层
useEffect(() => { // ✅ 顶层
if (count > 5) {
// 在这里进行条件判断
}
}, [count])
return <div />
}
2️⃣ 只在React函数中调用Hooks
// ❌ 错误:在普通函数中调用
function regularFunction() {
const [count, setCount] = useState(0) // 禁止!
}
// ✅ 正确:在函数组件中调用
function Component() {
const [count, setCount] = useState(0) // ✅
return <div />
}
// ✅ 正确:在自定义Hook中调用
function useCustomHook() {
const [count, setCount] = useState(0) // ✅
return count
}
3️⃣ 依赖数组要完整
function DependenciesDemo({ userId, filters }) {
// ❌ 错误:依赖不完整
useEffect(() => {
fetchUser(userId, filters)
}, [userId]) // filters变化不会重新获取
// ✅ 正确:包含所有依赖
useEffect(() => {
fetchUser(userId, filters)
}, [userId, filters])
// ✅ 使用useCallback时也要注意依赖
const handleSubmit = useCallback(() => {
console.log(userId, filters)
}, [userId, filters])
// ✅ 如果依赖是对象,考虑使用深层比较
const { data } = useFetch('/api/user', {
id: userId,
...filters
})
}
React Hooks都可以接收第二个参数,值是一个数组,底层通过Object.is(arr1,arr2)完成依赖项对比,对比逻辑为浅比较。
Object.is()方法用来判断两个值是否严格相等。与===的行为基本一致,但存在一些细微的差别。
Object.is()方法不会进行类型转换,会保持值的原始类型进行比较。
6. 知识点汇总
Q1:useEffect和useLayoutEffect的区别?
function EffectComparison() {
useEffect(() => {
// 异步执行,不阻塞浏览器绘制
console.log('useEffect: 在浏览器绘制后执行')
})
useLayoutEffect(() => {
// 同步执行,会阻塞浏览器绘制
console.log('useLayoutEffect: 在DOM更新后、浏览器绘制前执行')
})
return <div />
}
Q2:useMemo和useCallback的区别?
function MemoVsCallback() {
const [count, setCount] = useState(0)
// useMemo 缓存值
const computedValue = useMemo(() => {
return count * 2
}, [count])
// useCallback 缓存函数
const handleClick = useCallback(() => {
setCount(c => c + 1)
}, [])
return (
<div>
<p>计算值:{computedValue}</p>
<button onClick={handleClick}>增加</button>
</div>
)
}
Q3:useRef和createRef的区别?
function RefComparison() {
// useRef: 在整个生命周期中保持同一个引用
const ref1 = useRef(null)
// createRef: 每次渲染都会创建新的引用
const ref2 = React.createRef()
console.log('ref1不变:', ref1)
console.log('ref2每次变化:', ref2)
return <div ref={ref1} />
}
Hooks带来的革命性变化:
- 逻辑复用更简单:自定义Hook取代HOC和Render Props
- 代码更简洁:减少嵌套,相关逻辑集中
- 性能更好:细粒度的优化控制
- 学习成本低:只需掌握几个核心Hook