React Hook & Class

0 阅读11分钟

一、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组件的核心要点:

  1. 生命周期清晰:挂载、更新、卸载三阶段
  2. 性能优化手段丰富:PureComponent、shouldComponentUpdate
  3. 状态管理强大:setState的合并与批量更新
  4. 逻辑复用模式: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带来的革命性变化:

  1. 逻辑复用更简单:自定义Hook取代HOC和Render Props
  2. 代码更简洁:减少嵌套,相关逻辑集中
  3. 性能更好:细粒度的优化控制
  4. 学习成本低:只需掌握几个核心Hook