React Hooks

文章目录

  • 前言
  • [一、Hooks 的基本规则](#一、Hooks 的基本规则)
    • [1.1 两条核心规则](#1.1 两条核心规则)
    • [1.2 为什么有这些规则](#1.2 为什么有这些规则)
  • [二、常用 Hooks](#二、常用 Hooks)
    • [2.1 useState](#2.1 useState)
    • [2.2 useEffect](#2.2 useEffect)
    • [2.3 useContext](#2.3 useContext)
  • [三、useEffect vs useLayoutEffect](#三、useEffect vs useLayoutEffect)
    • [3.1 执行时机](#3.1 执行时机)
    • [3.2 执行顺序](#3.2 执行顺序)
    • [3.3 使用场景](#3.3 使用场景)
  • [四、自定义 Hook](#四、自定义 Hook)
    • [4.1 基本规则](#4.1 基本规则)
    • [4.2 自定义 Hook 的设计](#4.2 自定义 Hook 的设计)
  • [五、useMemo 与 useCallback](#五、useMemo 与 useCallback)
    • [5.1 useMemo:缓存计算结果](#5.1 useMemo:缓存计算结果)
    • [5.2 useCallback:缓存函数引用](#5.2 useCallback:缓存函数引用)
    • [5.3 滥用问题](#5.3 滥用问题)
  • 六、陈旧闭包问题
    • [6.1 问题描述](#6.1 问题描述)
    • [6.2 解决方案](#6.2 解决方案)
  • 七、易混淆点
  • 八、思考与练习
  • 总结

前言

上一篇讲了 Diff 算法;本篇进入 React Hooks------这是 React 16.8 引入的重要特性,彻底改变了 React 组件的编写方式。

Hooks 解决的核心问题是:

  1. 函数组件没有状态:之前只有类组件能管理状态
  2. 逻辑复用困难:HOC 和 Render Props 嵌套地狱
  3. 生命周期复杂:相关逻辑被拆散到不同生命周期方法

本篇会讲清楚:

  • Hooks 的基本规则
  • 常用 Hooks 的使用
  • useEffect 与 useLayoutEffect 的区别
  • 自定义 Hook 的设计

一、Hooks 的基本规则

1.1 两条核心规则

javascript 复制代码
// ❌ 错误:在条件语句中调用 Hook
function Component({ flag }) {
  if (flag) {
    const [count, setCount] = useState(0)  // 错误!
  }
}

// ❌ 错误:在循环中调用 Hook
function Component() {
  for (let i = 0; i < 3; i++) {
    const [count, setCount] = useState(0)  // 错误!
  }
}

// ✅ 正确:始终在组件顶层调用 Hook
function Component() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('Alice')
  
  // 条件逻辑放在 Hook 之后
  if (count > 10) {
    // ...
  }
}

规则总结

  1. 只在最顶层使用 Hook:不要在循环、条件或嵌套函数中调用
  2. 只在 React 函数组件或自定义 Hook 中调用:不要在普通函数中调用

1.2 为什么有这些规则

javascript 复制代码
// React 内部用数组存储 Hook 状态
let hooks = []
let index = 0

function useState(initialValue) {
  if (hooks[index] === undefined) {
    hooks[index] = initialValue
  }
  const currentIndex = index
  const setState = (newValue) => {
    hooks[currentIndex] = newValue
  }
  return [hooks[index++], setState]
}

// 每次渲染时,Hook 调用顺序必须一致
// 否则 index 会错乱,导致状态混乱

二、常用 Hooks

2.1 useState

javascript 复制代码
const [count, setCount] = useState(0)

// 函数式更新(推荐用于依赖旧值的场景)
setCount(prev => prev + 1)

// 惰性初始化(只在首次渲染执行)
const [state, setState] = useState(() => {
  return expensiveComputation()
})

2.2 useEffect

javascript 复制代码
useEffect(() => {
  // 副作用逻辑
  const timer = setInterval(() => {
    console.log('tick')
  }, 1000)
  
  // 清理函数(在组件卸载或依赖变化前执行)
  return () => {
    clearInterval(timer)
  }
}, [deps])  // 依赖数组

依赖数组规则

  • 无依赖:每次渲染后都执行
  • 空数组 []:只在首次渲染后执行
  • 有依赖 [a, b]:依赖变化后执行

2.3 useContext

javascript 复制代码
const ThemeContext = React.createContext('light')

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Child />
    </ThemeContext.Provider>
  )
}

function Child() {
  const theme = useContext(ThemeContext)  // 'dark'
  return <div className={theme}>Hello</div>
}

三、useEffect vs useLayoutEffect

3.1 执行时机

javascript 复制代码
// useEffect:在浏览器完成布局与绘制后异步执行
useEffect(() => {
  // 不阻塞浏览器渲染
})

// useLayoutEffect:在 DOM 变更后同步执行(阻塞渲染)
useLayoutEffect(() => {
  // 阻塞浏览器渲染,适合需要同步读取 DOM 布局的场景
})

3.2 执行顺序

javascript 复制代码
function Component() {
  // 1. 组件函数体执行
  console.log('render')
  
  // 2. DOM 更新
  // 3. 浏览器绘制
  
  useLayoutEffect(() => {
    console.log('useLayoutEffect')  // 先执行
  })
  
  useEffect(() => {
    console.log('useEffect')  // 后执行
  })
  
  return <div>Hello</div>
}

// 输出顺序:
// render
// useLayoutEffect
// useEffect

3.3 使用场景

javascript 复制代码
// ✅ useLayoutEffect:需要同步测量或修改 DOM
function Tooltip({ targetRef }) {
  const [position, setPosition] = useState({ x: 0, y: 0 })
  
  useLayoutEffect(() => {
    const rect = targetRef.current.getBoundingClientRect()
    setPosition({ x: rect.left, y: rect.bottom })
  }, [targetRef])
  
  return <div style={{ left: position.x, top: position.y }}>Tooltip</div>
}

// ✅ useEffect:异步副作用(数据请求、订阅、定时器)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null)
  
  useEffect(() => {
    fetchUser(userId).then(setUser)
  }, [userId])
  
  return <div>{user?.name}</div>
}

四、自定义 Hook

4.1 基本规则

javascript 复制代码
// 自定义 Hook 必须以 "use" 开头
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem(key)
    return saved ? JSON.parse(saved) : initialValue
  })
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])
  
  return [value, setValue]
}

// 使用
function App() {
  const [name, setName] = useLocalStorage('name', 'Alice')
  return <input value={name} onChange={e => setName(e.target.value)} />
}

4.2 自定义 Hook 的设计

javascript 复制代码
// 封装异步请求逻辑
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let cancelled = false
    
    const fetchData = async () => {
      setLoading(true)
      setError(null)
      
      try {
        const response = await fetch(url)
        const json = await response.json()
        
        if (!cancelled) {
          setData(json)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err)
        }
      } finally {
        if (!cancelled) {
          setLoading(false)
        }
      }
    }
    
    fetchData()
    
    // 清理函数:防止组件卸载后更新状态
    return () => {
      cancelled = true
    }
  }, [url])
  
  return { data, loading, error }
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
  
  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>
  return <div>{user.name}</div>
}

五、useMemo 与 useCallback

5.1 useMemo:缓存计算结果

javascript 复制代码
function App({ items, filter }) {
  // ❌ 每次渲染都重新计算
  const filteredItems = items.filter(item => item.includes(filter))
  
  // ✅ 只在 items 或 filter 变化时重新计算
  const filteredItems = useMemo(
    () => items.filter(item => item.includes(filter)),
    [items, filter]
  )
  
  return <List items={filteredItems} />
}

5.2 useCallback:缓存函数引用

javascript 复制代码
function App() {
  const [count, setCount] = useState(0)
  
  // ❌ 每次渲染都创建新函数,导致子组件重新渲染
  const handleClick = () => {
    setCount(count + 1)
  }
  
  // ✅ 缓存函数引用,子组件不会因函数变化而重新渲染
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1)
  }, [])
  
  return <Button onClick={handleClick} />
}

5.3 滥用问题

javascript 复制代码
// ❌ 错误:过度优化,简单计算不需要 useMemo
const fullName = useMemo(() => firstName + ' ' + lastName, [firstName, lastName])

// ✅ 正确:直接计算
const fullName = firstName + ' ' + lastName

// ❌ 错误:没有依赖变化的函数不需要 useCallback
const handleClick = useCallback(() => {
  console.log('click')
}, [])  // 永远不变,但增加了复杂性

// ✅ 正确:只有传递给 memo 子组件时才需要
const MemoizedChild = React.memo(Child)
function App() {
  const handleClick = useCallback(() => {
    console.log('click')
  }, [])
  return <MemoizedChild onClick={handleClick} />
}

六、陈旧闭包问题

6.1 问题描述

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count)  // 永远输出 0!
      setCount(count + 1)  // 永远设置为 1!
    }, 1000)
    
    return () => clearInterval(timer)
  }, [])  // 依赖数组为空,闭包捕获的 count 永远是 0
  
  return <div>{count}</div>
}

6.2 解决方案

javascript 复制代码
// 方案 1:添加依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1)
  }, 1000)
  
  return () => clearInterval(timer)
}, [count])  // count 变化时重新创建定时器

// 方案 2:使用函数式更新
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1)  // 使用最新的值
  }, 1000)
  
  return () => clearInterval(timer)
}, [])  // 无需依赖 count

// 方案 3:使用 useRef
function Counter() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  countRef.current = count  // 每次渲染更新 ref
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log(countRef.current)  // 读取最新值
      setCount(countRef.current + 1)
    }, 1000)
    
    return () => clearInterval(timer)
  }, [])
  
  return <div>{count}</div>
}

七、易混淆点

  1. useEffect 执行时机 :在浏览器完成布局与绘制后异步 执行,不阻塞渲染;useLayoutEffect 在 DOM 变更后同步执行,阻塞渲染。
  2. 依赖数组[] 表示只执行一次(首次渲染后);无依赖数组表示每次渲染后都执行。
  3. 陈旧闭包:useEffect 的闭包捕获的是创建时的值,不是最新的值。解决方案:添加依赖、使用函数式更新、或使用 useRef。
  4. useMemo vs useCallback :useMemo 缓存计算结果 ,useCallback 缓存函数引用
  5. 自定义 Hook:必须以 "use" 开头,本质是复用状态逻辑,不是复用状态本身。

八、思考与练习

1. 为什么 Hooks 不能在条件语句中调用?

解析:React 内部用数组存储 Hook 状态,依赖调用顺序定位状态。如果在条件语句中调用,渲染次数不同会导致调用顺序错乱,状态混乱。

2. useEffect 和 useLayoutEffect 的区别是什么?

解析:

  • useEffect :在浏览器完成布局与绘制后异步执行,不阻塞渲染
  • useLayoutEffect :在 DOM 变更后同步执行,阻塞渲染
  • 适用于需要同步测量或修改 DOM 的场景

3. 什么是陈旧闭包问题?如何解决?

解析:

javascript 复制代码
// 问题:useEffect 的闭包捕获的是创建时的值
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1)  // count 永远是 0
  }, 1000)
}, [])

// 解决方案:
// 1. 添加依赖 [count]
// 2. 使用函数式更新 setCount(prev => prev + 1)
// 3. 使用 useRef

4. 什么时候需要使用 useMemo?

解析:

  • 复杂计算:过滤、排序、转换大型数组
  • 传递给子组件:避免子组件因引用变化而重新渲染
  • 依赖其他 memoized 值:形成 memoized 链

简单计算(字符串拼接、简单数学运算)不需要使用。

5. 如何设计一个好的自定义 Hook?

解析:

  • 单一职责:一个 Hook 只做一件事
  • 命名清晰:以 "use" 开头,语义明确
  • 参数灵活:支持多种配置选项
  • 返回值简洁:返回对象或数组,便于解构
javascript 复制代码
// ✅ 好的设计
function useFetch(url, options = {}) {
  const { immediate = true } = options
  // ...
  return { data, loading, error, refetch }
}

// ❌ 不好的设计
function useData(url, method, headers, body, cache, retry) {
  // 参数过多,难以维护
}

6. useEffect 的清理函数什么时候执行?

解析:

  • 组件卸载时:组件从 DOM 中移除前
  • 依赖变化时:下次 effect 执行前(不是渲染前)
  • 不会在首次渲染后执行:首次渲染没有"上次 effect"需要清理

总结

  • Hooks 规则:只在顶层调用,不在条件/循环中调用
  • useEffect:异步执行,用于副作用(数据请求、订阅、定时器)
  • useLayoutEffect:同步执行,用于需要同步测量/修改 DOM 的场景
  • 自定义 Hook:复用状态逻辑,以 "use" 开头
  • useMemo/useCallback:缓存计算结果/函数引用,避免不必要的重新渲染
  • 陈旧闭包:闭包捕获的是创建时的值,通过依赖、函数式更新或 useRef 解决
相关推荐
数据知道1 小时前
C++ 层拦截:修改 Blink 引擎与 V8 绑定的底层逻辑
javascript·数据采集·指纹浏览器·风控
问心无愧05131 小时前
ctf show web入门110
前端·笔记
拉拉肥_King1 小时前
Vue 3 主题切换深度解析:从炫酷动画到零闪烁方案
前端·vue.js
excel1 小时前
为什么 Pinia + localForage 持久化后,页面初始化拿不到数据?
前端
雨雨雨雨雨别下啦1 小时前
vant介绍
前端
小小小小宇1 小时前
大模型失忆问题探讨
前端
wordbaby1 小时前
rn-cross-calendar:一个兼容 React 18/19、RN/RNOH 的跨平台日历组件
前端·react native·harmonyos
weixin_523185321 小时前
Collections.unmodifiableMap详解:真的不可修改吗?
java·linux·前端
江米小枣tonylua1 小时前
关掉 VSCode:在 NeoVim12 上配置 Claude Code
前端·程序员