React中的Hook到底是个什么鬼

文章目录

React Hooks 是 React 16.8 引入的函数组件增强机制,它允许你在不编写 class 的情况下使用 state 和其他 React 特性。
简单来说,Hooks 是 React 提供的一系列特殊函数,它们能让你"钩入"(hook into)React 的核心功能。

Hooks本质:

  • 函数组件的扩展工具:让函数式组件可以拥有类组件功能(状态、生命周期等)
  • 逻辑复用功能:代替高阶组件和render props的复杂模式
  • 代码组织方式:按照功能而非生命周期组织代码

Hooks的核心特点:

1、状态管理(useState)

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0); // 状态声明
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

函数式组件可以拥有自己的状态,React在内部维护了一个"记忆单元"链表,保存Hook的状态

2、副作用管理(useEffect)

javascript 复制代码
useEffect(() => {
  document.title = `You clicked ${count} times`;
  return () => { /* 清理逻辑 */ }; // 类似componentWillUnmount
}, [count]); // 依赖数组

代替了componentDidMount + componentDidUpdate + componentWillUnmount。可以将相关逻辑集中在一起,而非分散在不同生命周期上

3、上下文访问

javascript 复制代码
const value = useContext(MyContext); // 直接获取context值

无需再使用Consumer组价包裹

Hooks的设计原理

1、调用顺序稳定性

  • react依赖Hooks的调用顺序来正确关联状态
  • 因此Hooks不能放在条件判断、循环语句中

2、闭包机制

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 永远捕获声明时的count值
    }, 1000);
    return () => clearInterval(id);
  }, []);
  
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
  • 每次渲染都有独立的 props/state(闭包特性)
  • 通过函数式更新解决闭包问题:setCount(c => c + 1)

3、调度机制

  • useState/setState 会触发重新渲染
  • useEffect 在浏览器完成布局与绘制后异步执

Hooks 的底层实现(简化版)

javascript 复制代码
let hooks = [];
let currentHook = 0;

function useState(initialValue) {
  const _currentHook = currentHook++;
  if (hooks[_currentHook] === undefined) {
    hooks[_currentHook] = initialValue;
  }
  
  const setState = (newValue) => {
    hooks[_currentHook] = newValue;
    render(); // 触发重新渲染
  };
  
  return [hooks[_currentHook], setState];
}

function useEffect(callback, deps) {
  const _currentHook = currentHook++;
  const hasChanged = !deps || 
    !hooks[_currentHook] || 
    deps.some((d, i) => d !== hooks[_currentHook][i]);
  
  if (hasChanged) {
    callback();
    hooks[_currentHook] = deps;
  }
}

Hooks 的最佳实践

1、命名规范:自定义Hook必须以"use"开头

2、条件调用:只有在最顶层调用Hooks

3、性能优化

javascript 复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);

4、自定义Hooks:提取可复用逻辑

javascript 复制代码
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });
  useEffect(() => {
    const handler = () => setSize({ width: window.innerWidth });
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return size;
}

Hooks 与 Class 的对比

特性 Hooks Class Components
状态管理 useState/useReducer this.state
生命周期 useEffect componentDidMount 等
代码组织 按功能组织 按生命周期拆分
逻辑复用 自定义 Hooks HOC/render props
this绑定 无this绑定问题 需要处理this
学习曲线 较简单 较复杂

为什么Hooks依赖调用顺序来正确关联状态与组件实例

React Hooks 依赖调用顺序稳定性来正确关联状态与组件实例,这是其核心设计原理之一。这种限制源于 Hooks 的实现机制

一、底层实现机制

React在内部使用链表结构管理Hooks状态,每次渲染时:

1、按顺序记录Hooks调用(如useState->useEffect->useState)

2、用链表节点保存对应状态(节点位置与调用顺序严格对应)

3、下次渲染时候按相同顺序读取

就类似于一个军训的点名场景:

教官按照固定顺序进行点名(调用顺序)

每位学生(Hook)必须每次都站在队伍中相同的位置

教官通过位置编号而非名字来识别每一位学生

具体工作流程分析:

1、react内部会维护一个"记忆链表",结构如下:

useState → useEffect → useMemo → useState → ...

↓ ↓ ↓ ↓

state effect memoized state

2、渲染更新过程

组件首次渲染:

javascript 复制代码
function Example() {
  const [count, setCount] = useState(0);     // Hook 1
  const [name, setName] = useState('Alice'); // Hook 2
  useEffect(() => { /*...*/ });              // Hook 3
}

内部链表:

  1. useState(0) → 节点A { value: 0 }
  1. useState('Alice') → 节点B { value: 'Alice' }
  2. useEffect → 节点C { effectFn, deps }

组件更新渲染:

javascript 复制代码
function Example() {
  const [count, setCount] = useState(0);     // 读取节点A
  const [name, setName] = useState('Alice'); // 读取节点B
  useEffect(() => { /*...*/ });              // 读取节点C
}

工作流程:

1、按顺序遍历链表

2、第一个 useState 读取节点A的状态

3、第二个 useState 读取节点B的状态

4、useEffect 读取节点C的信息

二、条件语句为何破坏稳定性

javascript 复制代码
function MyComponent({ showExtra }) {
  if (showExtra) {
    const [extra, setExtra] = useState(null); // 🔴 条件性 Hook
  }
  const [count, setCount] = useState(0);     // ⚠️ 调用顺序可能变化
}

渲染过程:

当showExtra条件不变时候,第一次渲染和第二次渲染顺序就是一致的,当条件变化时候,顺序就会发生变化,如第一次为true,那么记录的位置是useState(null)->useState(0),当第二次为false时候,useState(0),那么第二次的位置就会读取到读取第一次的状态,这样值就是错乱的

三、同样的循环语句也会破坏稳定性

javascript 复制代码
function DynamicHooks({ items }) {
  return items.map((item, i) => {
    const [value, setValue] = useState(item); // 🔴 循环中的 Hook
    return <div key={i}>{value}</div>;
  });
}

1、Hooks调用次数变化:items.length改变会导致Hooks数量不一致

2、状态关联错乱:新增、删除等会使Hooks的读取错误位置的状态

四、React的Hooks必须在函数组件的最顶层且无条件的调用

1、状态关联错乱:Hooks与组件的状态是通过调用顺序关联的,条件调用会导致Hooks在不同条件渲染下对应到错误的状态

2、调试困难:难以追踪那些Hooks被调用、被跳过;导致难以复现的bug

3、性能优化失效:React依赖稳定的Hooks顺序进行优化,动态的Hooks会破坏memoization 和 bailout 机制

五、如何解决

1、编译时检查:通过ESLint 插件强制规则

javascript 复制代码
// eslint-disable-react-hooks/rules-of-hooks

2、运行时报错:开发模式下React会抛出错误

六、如何正确实现条件、循环逻辑

方式一:提前调用所有的Hook

javascript 复制代码
function MyComponent({ showExtra }) {
  const [extra, setExtra] = useState(null); // 始终调用
  const [count, setCount] = useState(0);

  return (
    <>
      {showExtra && <div>{extra}</div>}
      <div>{count}</div>
    </>
  );
}

方式二:拆分组件

javascript 复制代码
function ExtraComponent() {
  const [extra, setExtra] = useState(null); // 在独立组件中使用
  return <div>{extra}</div>;
}

function MyComponent({ showExtra }) {
  const [count, setCount] = useState(0);
  return (
    <>
      {showExtra && <ExtraComponent />}
      <div>{count}</div>
    </>
  );
}

如何理解Hooks中的闭包机制

React Hooks 的闭包机制是其核心设计特性之一,理解这一机制对于正确使用 Hooks 至关重要,下面详解:

一、闭包机制的本质

在 JavaScript 中,闭包是指函数能够访问并记住其词法作用域的特性。在 React 函数组件中:

1、每次渲染都是一个独立的函数调用

2、每次调用都会创建新的作用域和闭包

3、所有 props 和 state 都被"捕获"在该次渲染的闭包中

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

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count); // 永远捕获声明时的count值
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

渲染过程中闭包的表现:

1、首次渲染(count=0)

javascript 复制代码
// 闭包1
const count = 0;
useEffect(() => {
  // 这里的count永远为0
  setInterval(() => console.log(0), 1000);
}, []);

2、点击按钮后(count=1)

javascript 复制代码
// 闭包2
const count = 1;
// 但之前useEffect中的回调仍然引用闭包1的count(0)

3、事件函数中的闭包

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

  function handleClick() {
    setTimeout(() => {
      console.log(count); // 点击时的count值
    }, 3000);
  }

  return <button onClick={handleClick}>Click</button>;
}
  • 每次点击都会创建一个新的handleClick函数
  • 每个函数都捕获了当次渲染时的count值

解决闭包问题的方案:

1、函数式更新:

javascript 复制代码
setCount(c => c + 1); // 获取最新状态

2、useRef 保存可变值

javascript 复制代码
const countRef = useRef(count);
useEffect(() => {
  countRef.current = count; // 每次渲染后更新
});

// 在任何闭包中访问 countRef.current

3、使用 useReducer

javascript 复制代码
const [state, dispatch] = useReducer(reducer, { count: 0 });
// dispatch总是引用同一个函数

如何理解所有 props 和 state 都被"捕获"在该次渲染的闭包中

在上述代码中我们已经理解了闭包机制的本质。在React组件中,每次渲染都是一个独立的函数调用过程,这个过程会创建一个新的闭包作用域,在这个作用域中:

  • 所有的props和state值都会被"冻结",就想拍照一样,记录当下渲染时的值
  • 所有函数定义都会被绑到这个闭包中,包括事件函数、effect回调等
  • 即便外部组件重新渲染,这个闭包中的值也不会被改变,即保持渲染时的快照

举例:

javascript 复制代码
function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  
  function handleClick() {
    setTimeout(() => {
      console.log(`Count at render: ${count}, Prop: ${initialCount}`);
    }, 3000);
  }

  return (
    <div>
      <button onClick={handleClick}>Show Values</button>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
    </div>
  );
}

上述代码中假设initialCount 初始值是0

首次渲染时候,initialCount 为0 count也是0

当点击按钮后,count的值变为1 initialCount 依然是0

如果在第一次渲染后点击 Show Values,3秒后将打印

Count at render: 0, Prop: 0

即便组件已经更新 重新渲染了 但是旧的值依然是旧的值 因为在旧的闭包中仍然保持着它创建时的值