React Hooks 解密:带你玩转现代Web开发的利器

224 阅读6分钟

在现代前端开发中,React 无疑是最具影响力的框架之一。而随着 React 的不断发展,Hooks(钩子) 的引入彻底改变了我们编写组件的方式。它简化了状态管理和副作用处理,让函数组件具备了类组件所拥有的强大功能,同时避免了复杂的继承结构和繁琐的高阶组件。

1. useState - 状态管理

概念介绍: useState 定义一个响应式变量,提供专门的方法修改该变量的值

基本语法:

const [state, setState] = useState(initialState);
  • state: 当前状态值。
  • setState: 更新状态的方法,接受新状态作为参数或返回新状态的函数(推荐用于基于之前状态的更新)。
  • initialState: 状态的初始值。

案例分析:计数器

function Counter() {
  const [count, setCount] = useState(0); // 初始化计数为0

  return (
    <div>
      <p>当前计数: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>减少</button>
    </div>
  );
}

image.png 在这个例子中,每当用户点击按钮时,我们通过调用 setCount 方法更新 count 的值,这会导致组件重新渲染并显示最新的计数值。

2. useEffect - 副作用处理

概念介绍: useEffect Hook 可以让执行副作用操作,如数据获取、设置订阅以及手动更改 DOM。默认情况下,它会在每次渲染后运行,但可以通过提供依赖项数组来控制它的触发条件。

基本语法:

  • 首次渲染:无论依赖项是否有值,useEffect 都会在组件首次挂载时执行一次。
useEffect(() => {
  // effect (副作用代码)
  
  return () => {
    // cleanup (清理代码)
  };
}, [dependencies]);

三种主要使用方式

1. 无依赖数组 - 每次渲染后都执行

useEffect(() => {
  console.log('组件每次渲染后都会执行');
});

在 React 的 useEffect Hook 中,如果不传第二个参数(即不提供依赖数组) ,那么这个副作用函数将在组件每次完整渲染之后都会执行一次。这意味着它不仅会在首次挂载时运行,也会在每次状态更新、props 变化等引起的重新渲染后运行。

2. 空依赖数组 - 仅在挂载时执行

useEffect(() => {
  console.log('仅在组件挂载时执行一次');
  
  return () => {
    console.log('仅在组件卸载时执行一次');
  };
}, []);

在 React 的 useEffect Hook 中,当我们传入一个空数组作为依赖项(即 []),这个副作用函数将只在组件首次挂载时执行一次,并在组件卸载时运行清理函数。这种模式非常适合用于初始化和销毁阶段的操作。

3. 有依赖项 - 依赖变化时执行

useEffect(() => {
  console.log('当count变化时执行');
  
  return () => {
    console.log('在下一次effect执行前或组件卸载时清理');
  };
}, [count]);
  • 如果依赖数组中的值(如 count)发生了变化,React 会先调用上一次 effect 的清理函数(如果有的话),然后执行新的 effect。
  • 如果依赖项没有变化,则不会重新执行这个 useEffect

3. useLayoutEffect - 同步副作用

在 React 中,useLayoutEffectuseEffect 的“近亲”,但它有一个关键区别:它会在 DOM 更新之后、浏览器绘制之前同步执行。这意味着你可以在这个阶段安全地读取 DOM 布局信息(如宽高、位置等),并在绘制前进行调整。

useLayoutEffect(() => {
  // 同步操作 DOM 或测量布局
  return () => {
    // 清理逻辑(可选)
  };
}, [dependencies]);

为什么 useLayoutEffect 可能导致掉帧?

因为 useLayoutEffect 是同步执行的,且在浏览器绘制之前运行。如果其中的代码执行时间过长,会延迟浏览器的绘制过程,导致用户感知到界面卡顿或"掉帧"。

特性useEffectuseLayoutEffect
执行时机在浏览器绘制之后异步执行在 DOM 更新后、浏览器绘制之前同步执行
对渲染的影响不会阻塞浏览器渲染会阻塞浏览器渲染
使用场景大多数副作用场景需要同步读取/操作 DOM 的场景
性能影响较少影响性能可能导致掉帧(如果逻辑复杂)

4. useReducer - 相当于useEffect+useState

概念介绍: 在 React 开发中,当我们面对复杂的状态结构或多个子值之间存在多个互相关联的状态逻辑时,使用 useState 可能会导致组件内部状态管理混乱、难以维护。这时,useReducer 就成为了一个更优的选择。 基本语法:

const [state, dispatch] = useReducer(reducer, initialState);

案例分析:计数器应用

import React, { useReducer } from 'react';

const initialState = { count: 0 };

function reducer(state, action) {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            throw new Error();
    }
}

const Counter = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            Count: {state.count}
            <button onClick={() => dispatch({ type: 'increment' })}>+</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
        </div>
    );
};

export default Counter;

image.png 在这个例子中,我们使用 useReducer 实现了一个简单的计数器功能。点击按钮后,通过 dispatch 发送 incrementdecrement 动作,reducer 根据动作类型更新状态。

5. useRef - 获取DOM引用及可变值

概念介绍: useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。它可以用来访问 DOM 元素或保持任何可变值,而不会导致组件重新渲染。

const ref = useRef(initialValue);
  • ref.current 是一个可变属性,可以存储任意值(DOM 节点、数值、对象等)。

  • ref.current 的变化不会引起组件重新渲染。

  • 通常用于:

    • 访问 DOM 元素
    • 保存不需要参与渲染的状态数据
    • 在回调函数中捕获最新的状态或 props

案例分析:聚焦输入框

function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

在这个示例中,useRef 被用来创建一个引用,该引用指向输入框元素,使得可以通过编程方式聚焦到该输入框。

6. useContext - 跨组件通信

概念介绍: useContext Hook 接收一个上下文对象并返回当前上下文值。当你有两个或更多的嵌套层级且需要传递数据给子组件时,可以避免“prop drilling”。跨多层组件进行数据传递。

案例分析:主题切换

const ThemeContext = React.createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        切换主题
      </button>
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return <ThemedButton />;
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  
  return (
    <button style={{
      background: theme === 'dark' ? '#333' : '#eee',
      color: theme === 'dark' ? '#fff' : '#000'
    }}>
      我是{theme}主题的按钮
    </button>
  );
}

以上代码演示了如何利用 useContext 实现跨层级的主题切换功能,无需手动将 props 逐层传递下去。 被ThemeContext.Provider包裹的才可以用到父组件的值,

7. useMemo与useCallback - 处理缓存

子组件

export default memo(function Child({count,callback}) {   
    console.log('child render')
  return (
    <div>child{count}</div>
  )
})

父组件

export default function Memo() {
  const [n,setN] = useState(0)
  useEffect(()=>{
   setInterval(()=>{
    setN(Math.random())
   },1000)
  },[])

function cb(){
}
  return (
    <div>你好哦啊
      <h1>{n}</h1>
      <Child count={2} callback={cb}/> 
    </div>
  )
}

为什么明明父组件没变,但是还是会重新加载。因为每次渲染,都会返回一个新的函数。 解决方法

const cb = useCallback(function cb(){
  console.log('cb')
},[])

React中的useEffect与闭包机制的关系

export default function index() {
    const [count,setCount] = useState(0)
    useEffect(() => {
        setInterval(()=>{
            console.log(count)
            setCount(count+1)
        },1000)
    },[])
  return (
    <div>{count}</div>
  )
}

正常是不断的加1,实际上页面上面的数据一直是1,不会更新。

原因是

  • useEffect 在第一次渲染时执行
  • 此时 count = 0
  • useEffect 的依赖是 [],所以只执行一次setInterval 的回调函数还是原来的那个函数, 它记住的 count 仍然是第一次的 0
  • 即使后面 count 变成 1、2、3...,回调里访问的仍是第一次的 count(0)

👉 这就是闭包:函数记住了它创建时的变量环境。